Search code examples
inheritancedatacontractserializer

DatacontractSerializer does not serialize base type to inherited type


I have a TimeDetails class that has a collection of TimeRanges. This class is serialized and store to DB.

The list used to be of type DateTimeRange. Now I have to add a new property to it (SomeId). So I inherited it to AvailableTimeRange and added SomeId property to it. (I have to leave the DateTimeRange as-is since its used in other places.

The deserialization of the old rows (having DateTimeRange property type) fails.

Here is my unit test

 using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using mynamespace;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void With_DateTimeRange()
        {
            var xml = "<TimeDetails xmlns=\"http://schemas.datacontract.org/2004/07/mynamespace\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">" +
                      "<TimeList>" +
                      "<DateTimeRange><From>2016-01-21T08:00:00</From><To>2016-01-21T11:00:00</To></DateTimeRange>" +
                      "<DateTimeRange><From>2016-07-12T06:00:00</From><To>2016-07-12T09:00:00</To></DateTimeRange>" +
                      "</TimeList>" +
                      "</TimeDetails>";
            TimeDetails details = TimeDetails.FromXml(xml);
           Assert.AreEqual(2, details.TimeList.Count);
        }

        [TestMethod]
        public void With_AvailableTimeRange()
        {
            var xml =
            "<TimeDetails xmlns=\"http://schemas.datacontract.org/2004/07/mynamespace\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">"+
            "<TimeList>"+
            "<AvailableTimeRange><From>2016-07-12T08:00:00</From><SomeId>100</SomeId><To>2016-07-12T09:00:00</To></AvailableTimeRange>"+
            "</TimeList></TimeDetails>";

            var details = TimeDetails.FromXml(xml);
            Assert.AreEqual(1, details.TimeList.Count);
            Assert.IsTrue(details.TimeList[0] is AvailableTimeRange);
            Assert.AreEqual(100, (details.TimeList[0] as AvailableTimeRange).SomeId);
        }
    }
}

namespace mynamespace
{
    public class TimeDetails
    {
        public List<AvailableTimeRange> TimeList { get; set; }

        public static TimeDetails FromXml(string xml)
        {
            if (String.IsNullOrWhiteSpace(xml))
                return null;

            TimeDetails timeDetails;
            byte[] data = System.Text.Encoding.UTF8.GetBytes(xml);
            using (MemoryStream ms = new MemoryStream(data))
            {
                DataContractSerializer serializer = new DataContractSerializer(typeof (TimeDetails),
                    new[] {typeof (AvailableTimeRange), typeof (DateTimeRange)});
                timeDetails = (TimeDetails) serializer.ReadObject(ms);
            }
            return timeDetails;
        }

    }
    public class DateTimeRange
    {
        public DateTime From { get; set; }
        public DateTime To { get; set; }
    }

    public class AvailableTimeRange : DateTimeRange
    {
        public long? SomeId { get; set; }
    }
}

Solution

  • For backward compatibility you should leave list of DateTimeRange - it can store both base and derived types.

    public List<DateTimeRange> TimeList { get; set; }
    

    In xml use type attribute to identify AvailableTimeRange instances:

    <DateTimeRange i:type=\"AvailableTimeRange\"><From>2016-07-12T08:00:00</From><To>2016-07-12T09:00:00</To><SomeId>100</SomeId></DateTimeRange>
    

    Beware - order of members in xml is important.