§ August 10, 2005

C# Binary Serialization Oddities

Ok, it's been a while since I wrote about anything, so sue me!

Today's topic is an interesting find (at least to me)... When you serialize an object that has events (or a delegate) that have been assigned a value (and that value is outside of the class containing the event or delegate), .NET's binary serializer will attempt to serialize the enitre object who's function is assigned to it (it's an interesting thing to think about why they attempt to persist the event association as well). Said more simply, it will attempt to save the class being serialized, as well as all other classes who contain function definitions for those events or delegates.

Take the following example:

namespace SerializationWoes {
    using System;
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;
    class Class1 {
        Class1() {}

        [STAThread]
        static void Main(string[] args) {
            BinaryFormatter bf = new BinaryFormatter(); 
            byte[] bytes;
            BigDeal bd = new BigDeal();
            bd.Something = "blah blah blah";
            using ( MemoryStream ms = new MemoryStream() ) {
                bf.Serialize(ms, bd);
                ms.Seek(0,0);
                bytes = ms.ToArray();
            }
        }
    }

    // serialized object
    [Serializable]
    public class BigDeal {
        string something = "";
        public string Something {
            get { return this.something; }
            set { 
                this.something = value; 
            }
        }
        public BigDeal() {
        }
    }
}

BigDeal serializes perfectly into the memory stream and therefore to the byte array.

Now, let's throw in an event (and the event handler) with a non-serializable object who contains the definition of that event method handler:

namespace SerializationWoes {
    using System;
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;
    class Class1 {
        [NonSerialized]
        static Class1 c = new Class1();
        Class1() {} 
        private void bd_MyEvent(object sender, EventArgs e) {
            Console.WriteLine("hey some event just happened");

        } 

        [STAThread]
        static void Main(string[] args) {
            BinaryFormatter bf = new BinaryFormatter(); 
            byte[] bytes;
            BigDeal bd = new BigDeal();
            bd.MyEvent += new EventHandler(c.bd_MyEvent);
            bd.Something = "blah blah blah";
            using ( MemoryStream ms = new MemoryStream() ) {
                bf.Serialize(ms, bd);
                ms.Seek(0,0);
                bytes = ms.ToArray();
            }
        }
    }

    // serialized object
    [Serializable]
    public class BigDeal {
        string something = "";
        public event EventHandler MyEvent;
        protected virtual void OnEvent(EventArgs e) {
            if(MyEvent != null) MyEvent(this, e);
        }
        public string Something {
            get { return this.something; }
            set { 
                this.something = value; 
                OnEvent(EventArgs.Empty);
            }
        }
        public BigDeal() {
        }
    }
}


Now, when we run the app, we get the following exception:

[ Image of the exception ]


An unhandled exception of type 'System.Runtime.Serialization.SerializationException'
occurred in mscorlib.dll

Additional information: The type SerializationWoes.Class1 in Assembly 
SerializationWoes, Version=1.0.0.39766, Culture=neutral, PublicKeyToken=null 
is not marked as serializable.


What interests me here is *why* the binary formatter attempts to persist the event (and the other object, which contains the method for that event) as well.

A delegate / event is a function which is used like a variable. It can be assigned to and used just like a variable, so why not save its value as well. It makes sense when you think about it that way (Trust me. We spent far too much time wondering why some random UserControl was trying to be serialized into a custom object we had just written at work--until we realized that its events were being persisted as well). To persist a delegate or event, the framework also has to serialize the object whose function is the value of that event or delegate. Oddly enough, to do this, that object must be marked as [Serializable]. If not the above exception will be thrown every time you attempt to serialize your object containing the event.

There are 2 ways to get around this issue:
  • Make the class who subscribes to the event or delegate [Serializable] (if persisting that event / delegate is critical). This is often not the preferred choice.
  • Break your event out into a delegate / event Property. (as shown below)

The way to implement the latter is as follows. Change the public event to a private delegate, and a public event property. Our sample class changes to look like the following:
    using System.Runtime.CompilerServices;
	
    // serialized object
    [Serializable]
    public class BigDeal {
        string something = "";

        [NonSerialized]
        private EventHandler _myEvent;        
        public event EventHandler MyEvent {
            [MethodImpl(MethodImplOptions.Synchronized)]
            add {
                _myEvent = (EventHandler)Delegate.Combine(_myEvent, value);
            }
            [MethodImpl(MethodImplOptions.Synchronized)]
            remove {
                _myEvent = (EventHandler)Delegate.Remove(_myEvent, value);
            }
        }
        protected virtual void OnEvent(EventArgs e) {
            if(_myEvent != null) _myEvent(this, e);
        }
        public string Something {
            get { return this.something; }
            set { 
                this.something = value; 
                OnEvent(EventArgs.Empty);
            }
        }
        public BigDeal() {
        }
    }

As you can see, we've added a using statement for System.Runtime.CompilerServices, which is actually for the MethodImpl attributes placed in the add and remove blocks of the event property.

Next, we created a private delegate, and public event property for the event. We added the necessary Add & Remove blocks for the event property (events use add & remove to keep event's in sync) and add the MethodImpl attributes which take care of making the appropriate locks when we compile so that only 1 thread can access the event at a time to add and remove event definitions.

Next, we mark our delegate with the [NonSerialized] attribute, so that the serializer doesn't attempt to serialize the value of our event's delegate (aka our event handler).

The first suggestion--Marking your event handling class as [Serializable]--only works in a few situations, because we don't always have code for the base class we're extending (which also must be marked as serializable).

Here you have the final product which can have non-serializable objects define event handlers for it, and no exception will be thrown.
namespace SerializationWoes {
    using System;
    using System.IO;
    using System.Runtime.CompilerServices;
    using System.Runtime.Serialization.Formatters.Binary;
    class Class1 {
        [NonSerialized]
        static Class1 c = new Class1();
        Class1() {} 
        private void bd_MyEvent(object sender, EventArgs e) {
            Console.WriteLine("hey some event just happened");
        } 

        [STAThread]
        static void Main(string[] args) {
            BinaryFormatter bf = new BinaryFormatter(); 
            byte[] bytes;
            BigDeal bd = new BigDeal();
            bd.MyEvent += new EventHandler(c.bd_MyEvent);
            bd.Something = "blah blah blah";
            using ( MemoryStream ms = new MemoryStream() ) {
                bf.Serialize(ms, bd);
                ms.Seek(0,0);
                bytes = ms.ToArray();
            }
        }
    }

    // serialized object
    [Serializable]
    public class BigDeal {
        string something = "";
        [NonSerialized]
        private EventHandler _myEvent;        
        public event EventHandler MyEvent {
            [MethodImpl(MethodImplOptions.Synchronized)]
            add {
                _myEvent = (EventHandler)Delegate.Combine(_myEvent, value);
            }
            [MethodImpl(MethodImplOptions.Synchronized)]
            remove {
                _myEvent = (EventHandler)Delegate.Remove(_myEvent, value);
            }
        }
        protected virtual void OnEvent(EventArgs e) {
            if(_myEvent != null) _myEvent(this, e);
        }
        public string Something {
            get { return this.something; }
            set { 
                this.something = value; 
                OnEvent(EventArgs.Empty);
            }
        }
        public BigDeal() {
        }
    }
}


Now hopefully anyone having the same problem we had today at work (who has access to Google), won't have to spend as long with the problem as we had to (not that an hour or two is much time, just annoying).


Posted 20 years, 2 months ago on August 10, 2005

 Comments can be posted in the forums.

© 2003 - 2024 NullFX
Creative Commons Attribution-NonCommercial-ShareAlike 3.0 License