Partilhar via


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

Comments

  • Anonymous
    June 03, 2010
    I am currently Serializing STE's without any modifications att all the the T4 that generates the Entites. First I created a class to handle the Binary Serialization of whatever: public static class ByteArraySerializer {    public static byte[] ToByteArray<T>(this T graph)    {        var stream = new MemoryStream();        var serializer = new DataContractSerializer(typeof(T));        serializer.WriteObject(stream, graph);        return stream.ToArray();    }    public static T ToObject<T>(this byte[] bytes)    {        MemoryStream stream = new MemoryStream(bytes);        stream.Position = 0;        var serializer = new DataContractSerializer(typeof(T));        return (T)serializer.ReadObject(stream);    } } Then I created a Request Helper Class to stash the serialized bits in ASP.Net Session Object: public static Request Request        {            get            {                if (HttpContext.Current.Session["Request"] != null)                {                    byte[] bytes = (byte[])HttpContext.Current.Session["Request"];                    return bytes.ToObject<Request>();                }                else                {                    return null;                }            }            set            {                HttpContext.Current.Session["Request"] = ByteArraySerializer.ToByteArray(value);            }        } So in the ASP.net Web Calls on post I can do such: using (Service.ServiceClient service= new Service.ServiceClient())            {                request= Service.GetRequestData(RequestID);                RequestProvider.Requests.Request = request;            } I have tested it fairly extensively and it applies all the Related CRUD activity after Deserialization. What am I missing? The Object are already Marked with DataMember and DataContract to be paased via WCF no? Why then is [Serialzable] required?

  • Anonymous
    June 27, 2010
    Hi Jeff, do you also have a updated file for vb.net ? Kind Regards Christoph

  • Anonymous
    July 26, 2010
    Isn't this a really bad idea.  Are people REALLY storing huge objects like these in viewstate and making a 10KB page 100KB's due to increased VIEWSTATE? Why not just store a hashcode like NHibernate folks usually do in session state?  This approach is very low memory utilization but does require an additional hit to the database.

  • Anonymous
    August 09, 2010
    This works for ViewState but doesn't for Session.  I would rather store my objects in session for smaller payload between client and server.  Is there any chance for a template that will produce a class that would work for both ViewState and Session?

  • Anonymous
    August 30, 2010
    Hey e36m3, I've been trying this out for ViewState and it works fine without any changes.. but if you want to use in the Session, just call the StartTracking() method on the object before it is stored/changed.. seems to work fine. Thanks, David.

  • Anonymous
    September 18, 2010
    It would be much appreciated if you could point me to resource(s) or sample demonstrating updating say Order, Order_Detail and Product entities using the modified T4 template with WCF in for example asp.net web app.

  • Anonymous
    September 22, 2010
    Hello Jeff, I was interested to read your article on binary serializing STE, It would be much appreciated if you could point me to resources on using binary serializable STE for updating the database in asp.net web app via WCF. I am rather new to EF.

  • Anonymous
    December 29, 2010
    Hi Jeff, I'm using this for our service with Binary Serialization (WCF, WinForms), and it proved to be just the thing we we're missing. But also, since we're binding to the entities on winForms, and the things that attach to that PropertyChanged event shouldn't be serialized (or, in most cases can't be). So, a little adjustment needed to be made to your template by entering [field:NonSerializable] to events such as: _complexPropertyChanging _propertyChanged ObjectStateChanging Thanks, Veljko

  • Anonymous
    January 02, 2011
    The comment has been removed

  • Anonymous
    January 10, 2011
    Also, the ObservableCollection isn't marked [Serializable], and so the TrackableCollection that inherits it cannot be serialized because the INotifyPropertyChanged is implemented in ObservableCollection, and not in TrackableCollection - when someone subscribes to changes, he subscribes to the event in ObservableCollection and hence can't be serialized. So we had to tweak that a bit, and I'm sharing it here: public class TrackableCollection<T> : ObservableCollection<T>, INotifyPropertyChanged {    [field:NonSerialized]    public override event NotifyCollectionChangedEventHandler CollectionChanged;    [field: NonSerialized]    private PropertyChangedEventHandler _propertyChangedEventHandler;    event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged    {        add        {            _propertyChangedEventHandler = Delegate.Combine(_propertyChangedEventHandler, value) as PropertyChangedEventHandler;        }        remove        {            _propertyChangedEventHandler = Delegate.Remove(_propertyChangedEventHandler, value) as PropertyChangedEventHandler;        }    }    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)    {        NotifyCollectionChangedEventHandler handler = CollectionChanged;        if (handler != null)        {            handler(this, e);        }    } ... (the rest is the same)

  • Anonymous
    February 16, 2011
    We have really put this one to the test... there's one more thing that needs to be taken care of - when the objects are deserializing the "set" for ChangeTracker doesn't enter (because it's binary serialization) and hence, the ObjectStateChanging isn't subscribed to. So, in OnDeserialized event, you have to put if (_changeTracker != null) { _changeTracker.ObjectStateChanging -= HandleObjectStateChanging _changeTracker.ObjectStateChanging += HandleObjectStateChanging } so that the MarkAsDeleted() would get picked up and handled correctly (ie, removed from parent collection and placed in 'deleted' collection of the parent change tracker).

  • Anonymous
    April 20, 2011
    Nice article, I tried to use the Model.tt file at his bottom, but it still gives me serialization errors. So I followed suggestions from veljkoz but no way to put it working :-( Could anyone post a corrected version of the tt file please? Thank you for your help

  • Anonymous
    September 09, 2013
    This template is changed in EF 5.0, but the problem remains. I cannot use Entity objects in ViewState as they are not Serializable. Can you give me a hint how to solve this?