27 May 2011

Generic Serialization

Another side post, while I also work on the MVC application.

One of the things I deal with on a regular basis is serialization of objects to and from files. Additionally, I see the questions come up on StackOverflow from time to time. So, to save myself some time and effort, and, hopefully, to help educate others, I wrote a generic serialization .dll.

The project is very small at the moment, though I may expand it as I decide to explore other types of Streams. Right now, it is strictly file serialization/deserialization. The over-arching idea is the ISerializer interface which defines two methods: Serialize and Deserialize. Then each class will implement that interface. The reason I used an interface is that I really do plan to expand this code and use it out in the wild. For that reason, I need to be able to serialize to whatever Stream type is appropriate at the moment, but I need to de-couple the serialization from the objects, as well as the presentation and business layers, as possible.

So, on to the code.

As I said, I started with a simple Interface: ISerializer:

namespace GenericSerializer
{
    public interface ISerializer
    {
        void Serialize<T>(T target);
        T Deserialize<T>();
    }
}

Notice the use of the T type designator. I want this completely generic so that ANY class which is decorated as [Serializable] will work with my code. This won't do any custom serialization, but I've never found that much need for it.

Now, my two classes: GenericXmlSerializer and GenericBinarySerializer. Again, both of these serialize only to files, at the moment. That will probably change in the future.

Here is the Xml Serializer:

public class GenericXmlSerializer : ISerializer
    {
        public string Path { getset; }

        public GenericXmlSerializer() { }
        public GenericXmlSerializer(string path)
        {
            Path = path;
        }

        public void Serialize<T>(T target)
        {
            using (FileStream fs = new FileStream(this.Path, FileMode.Create))
            {
                XmlSerializer xs = new XmlSerializer(typeof(T));
                xs.Serialize(fs, target);
            }
        }

        public T Deserialize<T>()
        {
            T result;
            using (FileStream fs = new FileStream(this.Path, FileMode.Open))
            {
                XmlSerializer xs = new XmlSerializer(typeof(T));
                result = (T)xs.Deserialize(fs);
            }
            return result;
        }
    }

I elected to have a Path property because I have to have an instance anyway. So we may as well ask for the path to which a file will be serialized at the time of class instantiation. Also note that the serialization code itself is very, very basic. I just use the default settings of XmlSerializer to write out the file. Deserialization is the same, I'm presuming the type T will be defined such that basic deserialization will be okay. If it isn't, of course, I'll have a problem. But this code wasn't designed to handle custom serialization anyway, so I'm not terribly concerned.

I made the same decisions with the Binary Serializer:

public class GenericBinarySerializer : ISerializer
    {
        public string Path { getset; }

        public GenericBinarySerializer() { }
        public GenericBinarySerializer(string path)
        {
            Path = path;
        }

        public void Serialize<T>(T target)
        {
            using (FileStream fs = new FileStream(this.Path, FileMode.Create))
            {
                BinaryFormatter bf = new BinaryFormatter();
                bf.Serialize(fs, target);
            }
        }

        public T Deserialize<T>()
        {
            T result;
            using (FileStream fs = new FileStream(this.Path, FileMode.Open))
            {
                BinaryFormatter bf = new BinaryFormatter();
                result = (T)bf.Deserialize(fs);
            }
            return result;
        }
    }

Now, we have our serializers, but do they work? The only way to know is to set up another project which uses them. In this case, I'll use the same solution, but a new project. In the wild, of course, we'll be using different solutions with references to the .dll.

So, SampleSerializationProject: It has a Class: Employee, an Enum: WorkRole, and a simple console application that will put together a couple of files for us.

The Employee Class:

[Serializable]
    public class Employee
    {
        public int EmployeeId { getset; }
        public string LastName { getset; }
        public string FirstName { getset; }
          
        [XmlEnum]
        private WorkRole role;

        public WorkRole Role
        {
            get { return role; }
            set { role = value; }
        }
        public double Salary { getset; }
        public bool Exempt { getset; }

        public override string ToString()
        {
            return string.Format("Employee Id: {0} :: {1}, {2}", EmployeeId, LastName, FirstName);
        }
    }

Note that the WorkRole could not be represented as an automatically implemented property, I had to specify the backing field to use the [XmlEnum] attribute.

The WorkRole Enum:

   [Flags()]
    public enum WorkRole
    {
        Clerk,
        Laborer,
        Manager,
        Officer
    }

Here, note the [Flags()] attribute. This allows the binary formatter to serialize to and from the enum in the same way that [XmlEnum] allows the XmlSerializer to do so.

Finally, the program itself:

static void Main(string[] args)
        {
            var Employees = new List<Employee>
            {
                new Employee() {EmployeeId = 1, LastName = "Smith", FirstName = "John", Role = WorkRole.Clerk, Salary = 10.00, Exempt = false},
                new Employee() {EmployeeId = 2, LastName = "Doe", FirstName = "Jane", Role = WorkRole.Laborer, Salary = 15.00, Exempt = false},
                new Employee() {EmployeeId = 3, LastName = "Zachary", FirstName = "Marcus", Role  = WorkRole.Manager, Salary = 50000.00, Exempt = true},
                new Employee() {EmployeeId = 4, LastName = "Goodner", FirstName = "Allen", Role = WorkRole.Officer, Salary = 80000.00, Exempt =true}
            };

            GenericXmlSerializer gxs = new GenericXmlSerializer(@"C:\Testing\testXmlSerialization.xml");
            GenericBinarySerializer gbs = new GenericBinarySerializer(@"C:\Testing\testBinarySerialization.dat");

            gxs.Serialize<List<Employee>>(Employees);
            gbs.Serialize<List<Employee>>(Employees);

            Employees = null;
            List<Employee> Employees2;

            Employees = gxs.Deserialize<List<Employee>>();
            Employees2 = gbs.Deserialize<List<Employee>>();

            Console.WriteLine("Output from Xml Deserialization");
            Employees.ForEach(e => Console.WriteLine(e));
            Console.WriteLine();
            Console.WriteLine("Output from Binary Deserialization");
            Employees2.ForEach(e => Console.WriteLine(e));

            Console.ReadKey();
        }

Here you see I'm creating a List of Type: Employee. I then create two generic serializers (one for each type I built) and provide the path I where I want the files to go. I Serialize to disk, then I null out the first list and create a second- just to prove that both types worked. I deserialize from both files, and then write the output to the Console.

Please, feel free to copy this code to your heart's content. If anyone needs me to, I'll happily post the compiled .dll on Codeplex.

No comments:

Post a Comment