Blog Home  Home Feed your aggregator (RSS 2.0)  
ASP.NET AJAX Extensions Internals - Web.Config and Serialization - Manuel Abadia's ASP.NET stuff
 
# Sunday, 11 March 2007

Some time ago I started bloging about the Client Side OOP Features of Atlas (now the Microsoft AJAX Library. The posts are here and here).

As I think that most ASP.NET developers will use mainly the server side AJAX extensions I'll start covering the internals of the server side additions to ASP.NET to support AJAX.

If you create a new Web Site in VS2005 using the "ASP.NET AJAX Enabled Web-Site" you'll see that the web.config file for this web has quite a few additions to the default one related to AJAX. The additions to the web.config are:

1) Added some custom sections handlers to configure the new AJAX features

    <sectionGroup name="system.web.extensions" type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">

      <sectionGroup name="scripting" type="System.Web.Configuration.ScriptingSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">

        <section name="scriptResourceHandler" type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>

        <sectionGroup name="webServices" type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">

          <section name="jsonSerialization" type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="Everywhere"/>

          <section name="profileService" type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>

          <section name="authenticationService" type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" requirePermission="false" allowDefinition="MachineToApplication"/>

        </sectionGroup>

      </sectionGroup>

    </sectionGroup>

   

2) Configured the "asp" tag prefix for controls in the AJAX implementation assembly:

    <pages>

      <controls>

        <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

      </controls>

    </pages>

   

3) Added the AJAX assembly reference to the compilation process:

    <compilation debug="false">

      <assemblies>

        <add assembly="System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

      </assemblies>

    </compilation>

   

4) Modified the HTTP Handlers:

    <httpHandlers>

      <remove verb="*" path="*.asmx"/>

      <add verb="*" path="*.asmx" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

      <add verb="*" path="*_AppService.axd" validate="false" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

      <add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" validate="false"/>

    </httpHandlers>

   

5) Added an HTTP Module:

    <httpModules>

      <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

    </httpModules>

   

6) Added the configuration sections to configure ASP.NET AJAX (the section handlers were registered previously as explained in point 1).

<system.web.extensions>

    <scripting>

      <webServices>

        <!-- Uncomment this line to customize maxJsonLength and add a custom converter -->

        <!--

      <jsonSerialization maxJsonLength="500">

        <converters>

          <add name="ConvertMe" type="Acme.SubAcme.ConvertMeTypeConverter"/>

        </converters>

      </jsonSerialization>

      -->

        <!-- Uncomment this line to enable the authentication service. Include requireSSL="true" if appropriate. -->

        <!--

        <authenticationService enabled="true" requireSSL = "true|false"/>

      -->

        <!-- Uncomment these lines to enable the profile service. To allow profile properties to be retrieved

          and modified in ASP.NET AJAX applications, you need to add each property name to the readAccessProperties and

          writeAccessProperties attributes. -->

        <!--

      <profileService enabled="true"

                      readAccessProperties="propertyname1,propertyname2"

                      writeAccessProperties="propertyname1,propertyname2" />

      -->

      </webServices>

      <!--

      <scriptResourceHandler enableCompression="true" enableCaching="true" />

      -->

    </scripting>

  </system.web.extensions>


 

The most important additions to the web.config to support the Microsoft ASP.NET AJAX Extensions (MS AJAX from now on) are point 4 and 5. As the ASP.NET HTTP pipeline is very configurable, those additions can have severe implications on how request are handled, so I'll start with them.

In the HTTP handlers section, the first line removes the default handling for the asmx extension, and the second one registers a new handler for the asmx extension. This simple changes makes that any call to a web service doesn't work as we are used to. The asmx requests now will go through the ScriptHandlerFactory.

Usually in the HTTP handlers session we register a class that implements the IHttpHanlder interface. The ProcessRequest method will be called to do all processing.

However, we can also register a class that implements an IHttpHandlerFactory. In this case, the method GetHandler will be called in the factory, returning an IHttpHandler that will handle the request. This is used when you want to return different IHttpHandler instances depending on some conditions of the request.

When a request that is handled by the ScriptHandlerFactory arrives, it checks if the request is a REST request (REST is a way to transmit data over HTTP without using any additional messaging layer. Consult wikipedia for more information about it). If it is a REST request, the RestHandlerFactory (a MS AJAX class) will take care of the request. If it isn't, the WebServiceHandlerFactory will take care of it. Probably the class WebServiceHandlerFactory won't say much to you, but this is the class that handles ASMX request in the ASP.NET 2.0, so when an ASMX request isn't a REST request, it is handled in the same way as we're used to. 

What is a REST request for MS AJAX? A REST request is an HTTP request that has the ContentType property set to "application/json" (this is called a REST method request) or an HTTP request whose path ends with "/js" or "/jsdebug" (this is called a REST client proxy request).

The RestHandlerFactory checks if the request is for a client proxy or for a REST method. If the request is for a client proxy, a RestClientProxyHandler is created and servers the request. The RestClientProxyHandler figures which client proxy has been requested, gets the client proxy and sends it. All the hard work is done by the WebServiceClientProxyGenerator.GetClientProxyScript method. Before entering in detail how the client script proxy is generated, lets see how MS AJAX handles serialization.

Data types are exchanged between client side and server side using JSON (JavaScript Object Notation). To send data to the client side from the server, the data is serialized, and to receive it from the client, the data is deserialized in the server.

The main class responsible for this serialization/deserialization process is JavaScriptSerializer. The public member for the class are:

public class JavaScriptSerializer

{

    public JavaScriptSerializer();

    public JavaScriptSerializer(JavaScriptTypeResolver resolver);

 

    public int MaxJsonLength { get; set; }

    public int RecursionLimit { get; set; }

 

    public string Serialize(object obj);

    public void Serialize(object obj, StringBuilder output);

    public T Deserialize<T>(string input);

    public object DeserializeObject(string input);

 

    public T ConvertToType<T>(object obj);

    public void RegisterConverters(IEnumerable<JavaScriptConverter> converters);

}


 

The serialization fails if there are more nested objects than RecursionLimit or if the length of the serialized object is MaxJsonLength.

A serialized object can optionally include type information. The type information is provided by a class that inherits from JavaScriptTypeResolver, that has the following methods to get a type from a string and viceversa:

public abstract class JavaScriptTypeResolver

{

    public abstract Type ResolveType(string id);

    public abstract string ResolveTypeId(Type type);

}

 

The serializer will add the type information in a field called __type if it has an associated JavaScriptTypeResolver.

As the JavaScriptSerializer can not serialize/deserialize all types, you can supply a custom class to support types not directly supported by the JavaScriptSerializer using the RegisterConverters method. A converter has to inherit from:

public abstract class JavaScriptConverter

{

    public abstract IEnumerable<Type> SupportedTypes

    {

        get;

    }

 

    public abstract object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer);

    public abstract IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer);

}


   

and it has to implement the Serialize and Deserialize methods, as well as providing the list of supported types.

The standard MS AJAX distribution doesn't have any converter, but you can find a DataSet, DataTable and DataRow converter in the ASP.NET AJAX Futures release.

The other methods of the JavaScriptSerializer class are just serialize/deserialize variants, and the ConvertToType method that will be explained later.

The serialization of the basic types is as follows:

null or DBNull.Value -> "null"
bool -> "true" or "false"
char -> if it is '\0', "null", like a string if not
float, double -> ToString("r", CultureInfo.InvariantCulture)
DateTime -> "\/Date(number of milliseconds elapsed since midnight 1970/01/01 UTC)\/"
string -> quoted string
Guid -> "\" + ToString() + "\"
Uri -> "\" + Uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped) + "\"
other primitive types and Decimal -> IConvertible.ToString(CultureInfo.InvariantCulture)
Enum -> serialize the integer value of the enum
IDictionary -> JSON object
IEnumerable -> JSON array

A custom object is serialized as a JSON object with the optional member "__type" : typeName obtained using the JavaScriptResolver, where the other members are obtained from the public instance fields and public instance properties without the ScriptIgnore attribute applied.

for more information about JSON take a look here.

A Hashtable that compare references is used to throw an error if there is any circular reference.

Lets show an example of the serialization process. I'm going to use this User and Address class:

public class User

{

    private int _id;

    private string _name;

    private string _surName;

    private Address _address;

    private DateTime _creationDate;

 

    public int ID

    {

        get { return _id; }

        set { _id = value; }

    }

 

    public string Name

    {

        get { return _name; }

        set { _name = value; }

    }

 

    public string SurName

    {

        get { return _surName; }

        set { _surName = value; }

    }

 

    [ScriptIgnore]

    public string FullName

    {

        get { return Name + " " + SurName; }

    }

 

    public Address Address

    {

        get { return _address; }

        set { _address = value; }

    }

 

    public DateTime CreationDate

    {

        get { return _creationDate; }

        set { _creationDate = value; }

    }

 

    public User()

    {

    }

}

 

public class Address

{

    private string _firstLine;

    private string _secondLine;

    private string _country;

 

    public string FirstLine

    {

        get { return _firstLine; }

        set { _firstLine = value; }

    }

 

    public string SecondLine

    {

        get { return _secondLine; }

        set { _secondLine = value; }

    }

 

    public string Country

    {

        get { return _country; }

        set { _country = value; }

    }

 

    public Address()

    {

    }

}

 

With the following code:

JavaScriptSerializer js = new JavaScriptSerializer(new SimpleTypeResolver());

 

User usr = new User();

usr.ID = 12345;

usr.Name = "John";

usr.SurName = "Smith";

usr.Address = new Address();

usr.Address.FirstLine = "1st Avenue, 1234";

usr.Address.SecondLine = "San Diego, CA 92101";

usr.CreationDate = DateTime.Now;

 

string str = js.Serialize(usr);

  

The SimpleTypeResolver inherits from the JavaScriptTypeResolver class explained before, converting a Type a string using the AssemblyQualifiedName and viceversa.

Is serialized as:

{

    "__type":"User, AJAXServerSide1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",

    "ID":12345,

    "Name":"John",

    "SurName":"Smith",

    "Address":

        {

            "__type":"Address, AJAXServerSide1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",

            "FirstLine":"1st Avenue, 1234",

            "SecondLine":"San Diego, CA 92101",

            "Country":null

        },

    "CreationDate":"\/Date(1173541909382)\/"

}

 

Note that the FullName property of the User is not serialized because the ScriptIgnore attribute.

The deserialization process is delegated to the JavaScriptObjectDeserializer class. This class uses the JavaScriptString and the ObjectConverter class. The JavaScriptString class is a helper class used to tokenize the JSON data. As when the data is serialized all types are converted to JSON types (strings, numbers, JSON objects and JSON arrays) some kind of conversion back to the original type must take place. The JavaScriptObjectDeserializer does some simple conversions like checking if a string is a character, a boolean, a date or a null value, most significant conversions are handled by the ConvertObjectToTypeInternal method of the ObjectConverter class.

The ConvertObjectToTypeInternal uses the __type field, source object type and destination type to perform the convertion. If the source data is of type IDictionary it tries to create an object of the destination type and sets its fields and properties with the values stored in the dictionary. If the source data is of type IList, it tries to create a collection of the destination type and adds each of the items of the list to the collection. If the collection is a generic collection, it extract the item type of the collection and tries to convert the items as they're added to the collection.

If the source type isn't neither an IDictionary nor an IList, it gets the TypeConverter for the destination type and check if the source data can be converted to that type. If the source type doesn't have a conversion to the destination type, it converts the source type to a string and then tries to convert that string to the destination type (all of the conversion that involve the TypeConverter use the InvariantCulture).

If everything fails, a conversion exception is thrown.

To continue the previous example, this converts the string obtained from the Serialize method back to an User instance:

User u = js.Deserialize<User>(str);

 

When comparing the initial User instance with the deserialized one, the CreationDate is different by one hour (at least in my machine), so there is a bug in the MS AJAX code that handles DateTimes :-(

One curious thing about serialization is that the property names are case insensitive when deserializing an object, as the AssignToPropertyOrField method uses the BindingFlags.IgnoreCase to retrieve properties and fields. However, when an object is serialized, the properties for the client code are case sensitive.

I have run out of time for this week but soon I'll continue with the internals of the additions of MS AJAX to the web.config.

Sunday, 11 March 2007 19:10:25 (Romance Standard Time, UTC+01:00)  #    Comments [0]   Ajax | ASP.NET | JavaScript  | 
Copyright © 2018 Manuel Abadia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.