Silverlight 2: Doing Data Part VII: Custom Binary Serialization


By Peter Bromberg
Printer Friendly Version
View My Articles

    

Since Silverlight has no concept of "ADO.NET", DataReaders, DataSets or SqlConnections, it needs to work with data "over the wire" via WCF and ASMX webservices, usually in the form of typed Generic Lists or ObservableCollections. This is all XML.



"Large refactorings shouldn’t sneak up on you." -- Jeremy D. Miller

The standard serializers (DataContractSerializer, XmlSerializer) provided with Silverlight create XML for over - the - wire transport. The only exception is the DataContractJSONSerializer, which produces or consumes a more compact JSON-formatted string that can be Eval-ed as legal Javascript. But in the full .NET Framework, we have the BinaryFormatter, a complex set of classes and structs (66 of them, in all) that can take any Serializable object and serialize it into a compact byte array that is normally much smaller  in size.

When you are working with Silverlight data going over the wire, one of the biggest impediments is the time it takes for a verbose string of XML to go over and often for a similarly large one to come back in the response.  Binary Serialization allows us to transmit a very compact byte array back and forth over the wire, which increases both the speed and the scalability of an application.  The few milliseconds it takes to perform the binary serialization / deserialization on a collection of "Customers" for example, is tiny compared to the bandwidth savings of being able to transmit -- and receive back -- a much smaller payload. This means our app may be able to handle more connected clients and respond faster under load.

The CustomBinarySerializer classes I've created here are based on some work by Greg Young, an MVP with whom I have corresponded over the last year or two on this and similar topics. Greg is a real "thinker" and has produced a class that ports easily to the Silverlight Framework subset.

We start out with an interface that our classes must implement, in order to be "Custom Serializable":

public interface ICustomBinarySerializable
    {
        void WriteDataTo(BinaryWriter _Writer);
        void SetDataFrom(BinaryReader _Reader);
    }
Then, you implement this interface on your class, like this:
[Serializable]
    public class Customer : ICustomBinarySerializable
    {
        private String _lastname;
        private String _firstname;
        private String _address;
        private String _city;
        private string _region;
        private string _postalCode;
        private string _homePhone;

        public Customer()
        {
        }
        public Customer(String lastName, String firstName, String address,  string city, string region, string postalCode, string homePhone)
        {
            _lastname = lastName;
            _firstname = firstName;
            _address = address;
            _city = city;
            _region = region;
            _postalCode = postalCode;
            _homePhone = homePhone;
        }

        public String LastName
        {
            get { return _lastname; }
            set { _lastname = value; }
        }
        public String FirstName
        {
            get { return _firstname; }
            set { _firstname = value; }
        }
        public String Address
        {
            get { return _address; }
            set { _address = value; }
        }

        public String City
        {
            get { return _city; }
            set { _city = value; }
        }
        
        public String Region
        {
            get { return _region; }
            set { _region = value; }
        }

        public String PostalCode
        {
            get { return _postalCode; }
            set { _postalCode = value; }
        }


        public String HomePhone
        {
            get { return _homePhone; }
            set { _homePhone = value; }
        }

        public void WriteDataTo(BinaryWriter _Writer)
        {
            _Writer.Write((string)_lastname);
            _Writer.Write((string)_firstname);
            _Writer.Write((string)_address);
            _Writer.Write((string)_region);
            _Writer.Write((string)_postalCode);
            _Writer.Write((string) _homePhone);
        }

        public void SetDataFrom(BinaryReader _Reader)
        {
            _lastname = _Reader.ReadString();
            _firstname = _Reader.ReadString();
            _address = _Reader.ReadString();
            _region = _Reader.ReadString();
            _postalCode = _Reader.ReadString();
            _homePhone = _Reader.ReadString();
        }
    }
We can also create a "CustomerList" class on which we can implement the interface:
[Serializable]
    public class CustomerList: ICustomBinarySerializable
    {
        public List Customers;

        public CustomerList()
        {
            Customers = new List();
        }

        public void WriteDataTo(BinaryWriter _Writer)
        {
            foreach(Customer c in this.Customers )
            {
                _Writer.Write(c.LastName);
                _Writer.Write(c.FirstName);
                _Writer.Write(c.Address);
                _Writer.Write(c.City );
                _Writer.Write(c.Region);
                _Writer.Write(c.PostalCode);
                _Writer.Write(c.HomePhone);

            }
        }

        public void SetDataFrom(BinaryReader _Reader)
        {
            while (_Reader.BaseStream.Position < _Reader.BaseStream.Length )
            {
                Customer c = new Customer();
                c.LastName = _Reader.ReadString();
                c.FirstName = _Reader.ReadString();
                c.Address = _Reader.ReadString();
                c.City = _Reader.ReadString();
                c.Region = _Reader.ReadString();
                c.PostalCode = _Reader.ReadString();
                c.HomePhone = _Reader.ReadString();
                this.Customers.Add(c);
            }
        }
    }
Note that when deserializing, we don't know "how many" customer objects we've got, so we can simply continue to loop reading as long as the Reader's BaseStream position is less than its length.

We then have a CustomBinaryFormatter class, whose semantics should be familiar to any developer who has worked with custom Serialization and Serialization Surrogate classes:
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CustomBinarySerializer;

namespace CustomBinarySerializer
{
   public class CustomBinaryFormatter
   {
       private readonly MemoryStream m_WriteStream;
       private readonly MemoryStream m_ReadStream;
       private readonly BinaryWriter m_Writer;
       private readonly BinaryReader m_Reader;
       private readonly Dictionary m_ByType = new Dictionary();
       private readonly Dictionary m_ById = new Dictionary();
       private readonly byte[] m_LengthBuffer = new byte[4];
       private readonly byte[] m_CopyBuffer;
     
       public CustomBinaryFormatter()
       {
           m_CopyBuffer = new byte[20000];
           m_WriteStream = new MemoryStream(10000);
           m_ReadStream = new MemoryStream(10000);
           m_Writer = new BinaryWriter(m_WriteStream);
           m_Reader = new BinaryReader(m_ReadStream);
       }

       public void Register(int _TypeId) where T:ICustomBinarySerializable
       {
           m_ById.Add(_TypeId, typeof(T));
           m_ByType.Add(typeof (T), _TypeId);
       }

       public object Deserialize(Stream serializationStream)
       {
           if(serializationStream.Read(m_LengthBuffer, 0, 4) != 4)
               throw new SerializationException("Could not read length from the stream.");
           IntToBytes length = new IntToBytes(m_LengthBuffer[0], m_LengthBuffer[1], m_LengthBuffer[2], m_LengthBuffer[3]);
           //TODO make this support partial reads from stream
           if(serializationStream.Read(m_CopyBuffer, 0, length.i32) != length.i32) 
               throw new SerializationException("Could not read " + length.ToString() + " bytes from the stream.");
           m_ReadStream.Seek(0L, SeekOrigin.Begin);
           m_ReadStream.Write(m_CopyBuffer, 0, length.i32);
           m_ReadStream.Seek(0L, SeekOrigin.Begin);
           int typeid = m_Reader.ReadInt32();
           Type t;
           if(!m_ById.TryGetValue(typeid, out t))
               throw new SerializationException("TypeId " + typeid.ToString( ) + " is not a registerred type id");
           object obj = Activator.CreateInstance(t);
           ICustomBinarySerializable deserialize = (ICustomBinarySerializable) obj;
           deserialize.SetDataFrom(m_Reader);
           if(m_ReadStream.Position != length.i32) 
               throw new SerializationException("object of type " + t + " did not read its entire buffer during deserialization. This is most likely an inbalance between the writes and the reads of the object.");
           return deserialize;
       }

       public void Serialize(Stream serializationStream, object graph)
       {
           int key;
           if (!m_ByType.TryGetValue(graph.GetType(), out key))
               throw new SerializationException(graph.GetType() + " has not been registered with the serializer");
           ICustomBinarySerializable c = (ICustomBinarySerializable) graph; //this will always work due to generic constraint on the Register
           m_WriteStream.Seek(0L, SeekOrigin.Begin);
           m_Writer.Write((int) key);
           c.WriteDataTo(m_Writer);
           IntToBytes length = new IntToBytes((int) m_WriteStream.Position);
           serializationStream.WriteByte(length.b0);
           serializationStream.WriteByte(length.b1);
           serializationStream.WriteByte(length.b2);
           serializationStream.WriteByte(length.b3);
           serializationStream.Write(m_WriteStream.GetBuffer(), 0, (int) m_WriteStream.Position);
       }
   }
}
The "IntToBytes" struct call basically takes care of writing the length of the serialized stream which follows into the first four bytes - a very common construct in creating streams of bytes that are "self - describing".

To use the formatter, we first register our type, and then serialize our object into a MemoryStream. Here is a complete code snippet showing end-to-end operations:
private void PopulateCustomers()
        {
            // create a new CustomerList collection class
            CustomerList custs = new CustomerList();
            // Create and add three new Customer objects...
            Customer c = new Customer("doodad", "daddeo", "23 street", "Orlando", "FL", "32801", "4071235645");
            Customer c2 = new Customer("doodad2", "daddeo2", "24 street", "Orlando", "FL", "32802", "4071275645");
            Customer c3 = new Customer("doodad3", "daddeo3", "27 street", "Orlando", "FL", "32803", "4071235649");
            // add our objects to the Customers List in CustomerList...
            custs.Customers.Add(c);
            custs.Customers.Add(c2);
            custs.Customers.Add(c3);
            // Register the CustomerList type with the CustomBinaryFormatter
            CustomBinarySerializer.CustomBinaryFormatter f = new CustomBinaryFormatter();
            f.Register<CustomerList>(1);
            // Create a new stream to store our stuff
            MemoryStream ms = new MemoryStream();
            // Serialize the customer list (only takes up 168 bytes for three items!)
            f.Serialize(ms, custs);
            // rewind to beginning of stream!
            ms.Seek(0, 0);
            // Deserialize it into a new CustomerList object
            CustomerList cl2 = (CustomerList)f.Deserialize(ms);
            // Bind the grid--
            Grid1.ItemsSource = cl2.Customers;
        }

A CustomerList object containing three "customers" serializes down to just 168 bytes! According to Greg's tests, serializing with the custom serializer is 1612% faster than the BinaryFormatter,  and on deserializing it is 1418% faster. On Silverlight you probably would not see quite these large numbers.

To test this over the wire, I added a WCF Service to my Silverlight application. The service has copies of the Customer and CustomerList classes, plus a full-framework version of the CustomBinarySerializer library.  We accept a Northwind database Employees table query where the user has supplied the "Where" clause of the SQL in their WCF method call.

We create a SqlDataAdapter and get a DataSet comprising the results of the SQL query. We then iterate over the DataRows, creating and adding new Customer objects to the CustomerList. Finally, we serialize the CustomerList and send the resultant byte array back to the Silverlight app, which deserializes and populates a DataGrid with the resulting CustomerList's Customers field (a Generic List of type Customer):

[OperationContract]
        public byte[] GetEmployees( string whereClause)
        {
            byte[] b = null;
            string cnString =ConfigurationManager.ConnectionStrings["connectionString"].ConnectionString;
            if(String.IsNullOrEmpty( whereClause))
                whereClause = "1=1";
            string strSQL = "SELECT * FROM EMPLOYEES WHERE " + whereClause;
            SqlDataAdapter da = new SqlDataAdapter(strSQL, cnString);
            DataSet ds = new DataSet();
            da.Fill(ds);
            DataTable dtEmps = ds.Tables[0];

            CustomerList cl = new CustomerList();

            foreach(DataRow row in dtEmps.Rows)
            {
                string lastName = row["LastName"] != System.DBNull.Value ? (string) row["LastName"] : "";
                string firstName = row["FirstName"] != System.DBNull.Value ? (string)row["FirstName"] : "";
                string address = row["Address"] != System.DBNull.Value ? (string)row["Address"] : "";
                string city = row["City"] != System.DBNull.Value ? (string)row["City"] : "";
                string region = row["Region"] != System.DBNull.Value ? (string)row["Region"] : "";
                string postalCode = row["PostalCode"] != System.DBNull.Value ? (string)row["PostalCode"] : "";
                string  homePhone = row["HomePhone"] != System.DBNull.Value ? (string)row["HomePhone"] : "";
                Customer c = new Customer( lastName, firstName,address,city,region,postalCode,homePhone);
                cl.Customers.Add(c);
            }

            CustomBinarySerializer.CustomBinaryFormatter f = new CustomBinaryFormatter();
            f.Register<CustomerList>(1);
            MemoryStream ms = new MemoryStream();
            // Serialize the customer list (only takes up 168 bytes for three items!)
            f.Serialize(ms, cl);
            // rewind to beginning of stream!
            ms.Seek(0, 0);
            b = ms.ToArray();
            return b;
        }
For larger payloads, it may even make sense to apply Zip compression before sending the bytes over the wire. To add this, see my previous article here.  In the downloadable sample, I'm just getting all Customers, and I've provided some simple timings to show the speed of both "full" (XML) and "Binary". The timings won't be of much use though, unless you deploy the service to a remote machine so that the real payload speed "over the wire" can be measured.

You can download the complete solution here (updated for Silverlight 2 RTM) for your coding pleasure.


Biography - Peter Bromberg
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites.
Please post questions at forums, not via email!

button
 
Article Discussion: Silverlight 2 Beta 2: Doing Data Part VI: Custom Binary Serialization
Peter Bromberg posted at 27-Aug-08 12:51
Original Article

 
index feature
Wilson Chan replied to Peter Bromberg at 07-Oct-08 04:06
I added the index feature, anyone who like to have a look, check out the following link: http://files.cnblogs.com/unruledboy/Serializer.zip

 
Columns of type byte[]?
Andries Olivier replied to Peter Bromberg at 13-Nov-08 08:07

Good article! Let's say I've got an column which is of type bytes [] (varbinary) in the database, would I add a property in the customer class of type byte[], or as a string like you've done, even though some columns might be numeric?

My problem is that how do I read an array of bytes using the binaryreader specific to that row, as we have more than one customer? Meaning the readbytes method would not work, since we do not know the number of bytes pertaining to that column.

Cheers

Andries


 
Data types of varbinary
Andries Olivier replied to Wilson Chan at 14-Nov-08 02:30

Wilson,

How would I use this utility if my data types are varbinary stored in sql (byte[]), instead of just strings and numeric values?

BinaryReader.ReadBytes() would not work, as all we contain multiple rows, and do not want to read the entire stream.

Any help would be appreciated.

Andries


 
Reg: Custom Binary Serializer for Silverlight 2.0
Ven HS replied to Peter Bromberg at 25-Mar-09 11:54

Hi Peter,

             Your Customer class has only string properties. But my class has Properties of type Dictionary(of String, Object). How can I apply your GetDataTo and SetDataFrom methods in my class. Basically, my class will have properties which are List(of Object) and Dictionary(of string,Object) types. Please let me know how can I use the BinaryWriter and BinaryReader in such cases. Please let me know ASAP

Also, please post the VB.Net version of code

 


 
why m_CopyBuffer is set to a fixed size of 20000?
Vytautas V. replied to Peter Bromberg at 17-Jun-09 09:55
I would suggest to use m_CopyBuffer inside Deserialize method:

byte[] m_CopyBuffer = new byte[length.i32];
if (serializationStream.Read(m_CopyBuffer, 0, length.i32) != length.i32)

Why?

Because if you're deserializing a small object, then it is not necessary to allocate a memory for 20000 bytes;
this is especially a huge perfomrance decrease if using on lets say a List of serialized objects.

  

Search

search



Purchase