Using Binary Serialization and ViewState with Self-Tracking Entities

A common question the Entity Framework team hears is how to store a set of self-tracking entities in ViewState for an ASP.NET application. There are a number of options for where to store state information in an ASP.NET and I’d recommend this article on MSDN if you are looking for guidance on this. In this post, I’ll use “ViewState” as a representative of the options that are available.

 

The default T4 code generation template for self-tracking entities does not have the necessary infrastructure to support binary serialization which is what is needed for the entities to be stored in ViewState. The nice thing about T4 templates is that you can make your own changes to customize the code that is generated for your entities, so it is not hard to add binary serialization support to your entity template. The basic steps are to:

1. Add the [Serializable] attribute to the entity, complex type, and collection classes.

2. Add the required serialization constructors to the Dictionary collection classes

3. Add some code to the OnDerserializedMethod of the entites and complex types to hook up the change tracking events when an entity is deserialized.

 

The primary reason these were left out of the T4 template in Visual Studio 2010 is because binary serialization is not yet available in Silverlight, so if you make the changes for #1 and #2 above, the template will not produce code that compiles with Silverlight. If you want to see or use an updated template that supports binary serialization, the bottom of this post has a link to a C# self-tracking entity template with a flag at the top of the template to turn these capabilities on or off. The rest of this post describes the changes I made to the default self-tracking entities template to add binary serialization support.

 

Serializable Classes

The self-tracking entity template adds two templates to your project: one that generates the entities, and one that generates the ObjectContext (which typically has the name <Model>.Context.cs). All of the changes you will need to make are in the template that generates the entities, so we’ll be focusing on how to update that template in this post.

 

The first thing that is needed is to mark all classes that will be serialized in ViewState with the [Serializable] attribute. To add this attribute to entity classes, look for line 37 where entity classes are defined and add the attribute above it (note: throughout this post, code line numbers refer to the line of the change assuming you have made all previous changes to the template):

37: [Serializable]

38: <#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>

    partial class <#=code.Escape(entity)#><#=code.StringBefore(" : ",

    code.Escape(entity.BaseType))#><#=entity.BaseType == null ? ": " : ",

    "#>IObjectWithChangeTracker, INotifyPropertyChanged

39: {

 

We’ll need to do the same thing for complex type classes, which is on line:

1114: [Serializable]

1115: <#=Accessibility.ForType(complex)#> partial class <#=code.Escape(complex)#> :

    INotifyComplexPropertyChanging, INotifyPropertyChanged

1116: {

 

There are also a number of support classes that assist with the change tracking aspect of self-tracking entities, and these will need to be marked with the [Serializable] attribute as well. You can find these classes here:

1403: [Serializable]

1404: public class TrackableCollection<T> : ObservableCollection<T>

1405: {

 

1428: [Serializable]

1429: public class ObjectChangeTracker

1430: {

 

1650: [Serializable]

1651: public class ObjectsAddedToCollectionProperties : Dictionary<string, ObjectList>

      { }

 

1655: [Serializable]

1666: public class ObjectsRemovedFromCollectionProperties : Dictionary<string,

      ObjectList> { }

 

1660: [Serializable]

1661: public class OriginalValuesDictionary : Dictionary<string, Object> { }

 

1665: [Serializable]

1666: public class ExtendedPropertiesDictionary : Dictionary<string, Object> { }

 

1669: [Serializable]

1670: public class ObjectList : List<object> { }

Serialization Constructors

The next step is to ensure that the classes that derive from Dictionary<> are fully serializable. The Dictionary<>class implements its own custom serialization using ISerializable and so requires all derived classes to have a special constructor for deserialization. There isn’t any additional code we have to add to these constructors, they just need to be there so the Dictionary<> classes can be deserialized. There are four classes that need this constructor. Below is the template code for these classes with the serialization constructor and a default constructor added.

 

The ObjectsAddedToCollectionProperties class:

1651: public class ObjectsAddedToCollectionProperties : Dictionary<string, ObjectList>

1652: {

1653: public ObjectsAddedToCollectionProperties() { }

1654:

1655: protected ObjectsAddedToCollectionProperties(SerializationInfo info,

1656: StreamingContext ctx)

1657: : base(info, ctx)

1658: { }

1659: }

The ObjectsRemovedFromCollectionProperties class:

1664: public class ObjectsRemovedFromCollectionProperties : Dictionary<string,

      ObjectList>

1665: {

1666: public ObjectsRemovedFromCollectionProperties() { }

1667:

1668: protected ObjectsRemovedFromCollectionProperties(SerializationInfo info,

1669: StreamingContext ctx)

1670: : base(info, ctx)

1671: { }

1672: }

The OriginalValuesDictionary class:

1677: public class OriginalValuesDictionary : Dictionary<string, Object>

1678: {

1679: public OriginalValuesDictionary() { }

1680:

1681: protected OriginalValuesDictionary(SerializationInfo info,

1682: StreamingContext ctx)

1683: : base(info, ctx)

1684: { }

1685: }

 

And finally, the ExtendedPropertiesDictionary class:

1690: public class ExtendedPropertiesDictionary : Dictionary<string, Object>

1691: {

1692: public ExtendedPropertiesDictionary() { }

1693:

1694: protected ExtendedPropertiesDictionary(SerializationInfo info,

1695: StreamingContext ctx)

1696: : base(info, ctx)

1697: { }

1698: }

 

Event Hookup

The self-tracking entities will now serialize and deserialize, but we need to make sure all of the change tracking events are hooked up because binary serialization does not serialize event handlers. However, binary serialization does provide an OnDeserialized method to run some of our own code once all of the entities have been fully deserialized. Self-tracking entities already take advantage of this to turn on change tracking when entities are deserilaized, so what we need to do is to add code to the [OnDeserialized] method for entities to make sure events are hooked up properly.

 

To hook up an event properly during deserialization can be a bit tricky, however. Self-tracking entities typically hooks up all events as part of the public property setters. Binary serialization does not use the public property setters which is why the events are not hooked up by default, but WCF data contract serialization does use the public properties and so will hook up the events. WCF data contract serialization also will call the [OnDeserialized] method, which means we need to make sure that the event is not already hooked up before we try to add the event handler (otherwise the handler would be hooked up twice, and so called twice when the event fires). Unfortunately, there really isn’t a way to check to see if the event is already hooked up or not, so the best thing to do is to first remove the event handler, and then add it back. If the event handler isn’t there (in binary serialization), then the remove turns into a no-op and there is no danger of this throwing an exception. If the event handler is already there (in WCF serialization), then it will be removed and then re-added. The end result is that the event handler is always hooked up properly.

 

To make this change for entity types, find the OnDeserializedMethod on line 440 of the updated template. There are three kinds of event hook ups that are important for entity types:

· Complex type property event handlers

· Collection property event handlers for relationship fixup of bidirectional relationships

· Cascade delete event handlers

 

You can replace the OnDeserializedMethod with this bit of T4 template code to wire-up all of this fixup:

[OnDeserialized]

public void OnDeserializedMethod(StreamingContext context)

{

    IsDeserializing = false;

    ChangeTracker.ChangeTrackingEnabled = true;

<#

// Hook up ComplexType property event handlers

foreach(EdmProperty edmProperty in entity.Properties

    .Where(p => p.TypeUsage.EdmType is ComplexType && p.DeclaringType == entity))

{

#>

    if (<#=code.FieldName(edmProperty)#> != null)

    {

        ((INotifyComplexPropertyChanging)<#=code.FieldName(edmProperty)#>)

             .ComplexPropertyChanging -= Handle<#=edmProperty.Name#>Changing;

        ((INotifyComplexPropertyChanging)<#=code.FieldName(edmProperty)#>)

             .ComplexPropertyChanging += Handle<#=edmProperty.Name#>Changing;

    }

<#

}

// Hook up Collection property event handlers

foreach (NavigationProperty navProperty in entity.NavigationProperties

    .Where(np => np.DeclaringType == entity))

{

    if (navProperty.ToEndMember.RelationshipMultiplicity ==

                                          RelationshipMultiplicity.Many)

    {

#>

    if (<#=code.FieldName(navProperty)#> != null)

    {

        <#=code.FieldName(navProperty)#>.CollectionChanged -= Fixup<#=navProperty.Name#>;

        <#=code.FieldName(navProperty)#>.CollectionChanged += Fixup<#=navProperty.Name#>;

<#

  if (ef.IsCascadeDeletePrincipal(navProperty))

        {

#>

        // This is the principal end in an association that performs cascade deletes.

        // Add the cascade delete event handler for any entities that are

        // already in the collection.

        foreach (var item in <#=code.FieldName(navProperty)#>)

        {

       ChangeTracker.ObjectStateChanging -= item.HandleCascadeDelete;

            ChangeTracker.ObjectStateChanging += item.HandleCascadeDelete;

        }

<#

        }

#>

    }

<#

    }

}

#>

}

 

To give an example of what this outputs, I changed the basic Northwind model to include a complex type on Customer and made the Customer-Orders association have a cascade delete behavior. This is what the OnDeserializedMethod looks like:

[OnDeserialized]

public void OnDeserializedMethod(StreamingContext context)

{

    IsDeserializing = false;

    ChangeTracker.ChangeTrackingEnabled = true;

    if (_place != null)

    {

        ((INotifyComplexPropertyChanging)_place).ComplexPropertyChanging -=

                                                 HandlePlaceChanging;

        ((INotifyComplexPropertyChanging)_place).ComplexPropertyChanging +=

                                                 HandlePlaceChanging;

    }

    if (_orders != null)

    {

    _orders.CollectionChanged -= FixupOrders;

        _orders.CollectionChanged += FixupOrders;

    // This is the principal end in an association that performs cascade deletes.

    // Add the cascade delete event handler for any entities that are

    // already in the collection.

    foreach (var item in _orders)

    {

    ChangeTracker.ObjectStateChanging -= item.HandleCascadeDelete;

    ChangeTracker.ObjectStateChanging += item.HandleCascadeDelete;

    }

    }

    if (_customerDemographics != null)

    {

    _customerDemographics.CollectionChanged -= FixupCustomerDemographics;

        _customerDemographics.CollectionChanged += FixupCustomerDemographics;

    }

}

 

In Summary

After making the above changes to the entity type T4 template for self-tracking entities, your entities can be serialized and deserialized using the BinaryFormatter which is used in ASP.NET’s ViewState engine or by other scenarios where binary serialization is required.  The link at the bottom of this post has a version of the C# template where all of the above changes have been made.  Note that you'll need to drop the ".txt" from the extension so that the filename is just "Model.tt". If you have any feedback on self-tracking entities or questions about how to use them in various scenarios, let me know and I can recommend additional best practices.

 

Jeff Derstadt

Entity Framework Team

Model.tt.txt