14

What I have is a custom settings file that I serialize/deserialize using an XmlSerializer. I have no schema defined and no serialization tags in my object definition, just straight object serialization (although I will add them if needed).

My issue is that I need to add data members to the object. If I do that I know that the old settings file will not deserialize.

Is there a way to specify default values for the added members or some simple way to ignore them if they are missing from the XML?

Mike Webb
  • 8,855
  • 18
  • 78
  • 111
  • Do you need to add members to the _class_ or are you injecting members into a dynamic object? if it's the former you can simply initialize the members to a default value – Rune FS Nov 17 '11 at 22:00
  • `I know that the old settings file will not deserialize`, No, just your newly added fields/properties will get the default value(or the value you give in the constructor) – L.B Nov 17 '11 at 22:07
  • XmlSerializer does support [DefaultValue], but it uses it during serialization. During deserialization, it simply runs the constructor. See my updated answer for a way to handle default values based on versions – Dustin Davis Nov 18 '11 at 16:49

7 Answers7

7

From MSDN

Best Practices To ensure proper versioning behavior, follow these rules when modifying a type from version to version:

  • When adding a new serialized field, apply the OptionalFieldAttribute attribute.

  • When removing a NonSerializedAttribute attribute from a field (that was not serializable in a previous version), apply the OptionalFieldAttribute attribute.

  • For all optional fields, set meaningful defaults using the serialization callbacks unless 0 or null as defaults are acceptable.

I have tried to simulate your case where in new version of class have new member named Element2. initialized my new member to "This is new member" here is full proof

Test1 assumes you serialized with old definition of Root class with Just one Element1

Test2 when you serialized and de serialized with new definition of Root Class

To answer your question any way to provide default values you should use "OptionalField"

using System;
using System.Runtime.Serialization;
using System.IO;

public class Test
{

  [Serializable]
  public class Root
  {
    [OptionalField(VersionAdded = 2)] // As recommended by Microsoft
    private string mElement2 = "This is new member";
    public String Element1 { get; set; }    
    public String Element2 { get { return mElement2; } set { mElement2 = value; } }
  }

  public static void Main(string[] s)
  {
    Console.WriteLine("Testing serialized with old definition of Root ");
    Console.WriteLine(" ");
    Test_When_Original_Object_Was_Serialized_With_One_Element();
    Console.WriteLine(" ");
    Console.WriteLine("Testing serialized with new definition of Root ");
    Console.WriteLine(" ");
    Test_When_Original_Object_Was_Serialized_With_Two_Element();
    Console.ReadLine();
  }

  private static void TestReadingObjects(string xml)
  {
    System.Xml.Serialization.XmlSerializer xmlSerializer =
    new System.Xml.Serialization.XmlSerializer(typeof(Root));


    System.IO.Stream stream = new MemoryStream();
    System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
    Byte[] bytes = encoding.GetBytes(xml);
    stream.Write(bytes, 0, bytes.Length);
    stream.Position = 0;
    Root r = (Root)xmlSerializer.Deserialize(stream);

    Console.WriteLine(string.Format("Element 1 = {0}", r.Element1));

    Console.WriteLine(string.Format("Element 2 = {0}", r.Element2 == null ? "Null" : r.Element2));
  }
  private static void Test_When_Original_Object_Was_Serialized_With_One_Element()
  {
    TestReadingObjects(@"<Root>   <Element1>1</Element1>   </Root>");
  }

  private static void Test_When_Original_Object_Was_Serialized_With_Two_Element()
  {
    TestReadingObjects(@"<Root>   <Element1>1</Element1> <Element2>2</Element2>   </Root>");
  }
}

// here is the output enter image description here

Surjit Samra
  • 4,614
  • 1
  • 26
  • 36
  • This isn't fail safe. It would be way better to add a version attribute to the root and only check for the version number, then use a strategy parrten to deserialize from there. Your example is easily broken. – Dustin Davis Nov 17 '11 at 23:41
  • That was just a simple sample to prove it is possible de serialize with new definition of class. I do know about onserialized attributes but i thought that was not his question here is full sample i did few days ago which runs through WCF to handle uninitialized member variables http://stackoverflow.com/questions/8084868/how-to-use-custom-serialization-or-deserialization-in-wcf-to-force-a-new-instanc/8086306#8086306 – Surjit Samra Nov 18 '11 at 00:08
  • You're forcing a WCF solution when there is no mention of WCF in his question. – Dustin Davis Nov 18 '11 at 04:07
  • I am not forcing WCF solution, That is an example how you can override default of serialization and deserilization.He can skip WCF and look at how class Data is decorated with attributes and how overriding deserialization helps to provide default values. – Surjit Samra Nov 18 '11 at 07:35
  • @DustinDavis I am still waiting for your fail safe version attribute checking and using strategy pattern ? This is a learning site not just criticise and then hide away – Surjit Samra Nov 18 '11 at 09:37
  • Search engines are a wonderful thing, you should try them some time. See my updated answer. – Dustin Davis Nov 18 '11 at 16:42
  • Now who needs search engine if we got masters like you. May be you should provide your source(as you said try search engine) with your answer as well for more knowledge :) – Surjit Samra Nov 18 '11 at 17:01
  • if you have questions, comments or concerns about my answer, I'll be more than happy to answer them. Master? No. – Dustin Davis Nov 18 '11 at 17:05
  • I will have once I am able to run it, May be if you can provide full solution where you first serialize as at the moment it is trying to find a file on my system which does not exists for obvious reason :( – Surjit Samra Nov 18 '11 at 17:12
  • Updated my answer following best practices from Microsoft MSDN here http://msdn.microsoft.com/en-us/library/ms229752(v=vs.80).aspx – Surjit Samra Nov 19 '11 at 06:49
  • Nice one, I'll have to check out that attribute. But, you still aren't solving the OP's issue, providing default values when deserializing older values. XmlSerializer will deserialize a model even if it has elemtns that don't map. – Dustin Davis Nov 19 '11 at 06:56
  • Did you check results of Test1 – Surjit Samra Nov 19 '11 at 07:01
5

It should deserialize just fine, it will just use the default constructor to initialize the items. So they will remain unchanged.

McKay
  • 12,334
  • 7
  • 53
  • 76
4

You need to manually handle it using custom methods and marking them with the appropriate attributes OnSerializing/OnSerialized/OnDeserializing/OnDeserialized and manully determine how to initialize the values (if it can be done)

http://msdn.microsoft.com/en-us/library/system.runtime.serialization.ondeserializingattribute.aspx http://msdn.microsoft.com/en-us/library/system.runtime.serialization.ondeserializedattribute.aspx http://msdn.microsoft.com/en-us/library/system.runtime.serialization.onserializedattribute.aspx http://msdn.microsoft.com/en-us/library/system.runtime.serialization.onserializingattribute.aspx

a better way is to determine the version before hand and use a strategy pattern to do the deserialization. This isn't always possible so use what I suggest in that case.

Update: The previous answer is only applicable to binary serialization. For regular Xml you can use this method.

class Program
    {
        static void Main(string[] args)
        {
            Deserialize(@"..\..\v1.xml");
        }

        private static Model Deserialize(string file)
        {
            XDocument xdoc = XDocument.Load(file);
            var verAtt = xdoc.Root.Attribute(XName.Get("Version"));
            Model m = Deserialize<Model>(xdoc);

            IModelLoader loader = null;

            if (verAtt == null)
            {
                loader = GetLoader("1.0");
            }
            else
            {
                loader = GetLoader(verAtt.Value);
            }

            if (loader != null)
            {
                loader.Populate(ref m);
            }
            return m;
        }

        private static IModelLoader GetLoader(string version)
        {
            IModelLoader loader = null;
            switch (version)
            {
                case "1.0":
                    {
                        loader = new ModelLoaderV1();
                        break;
                    }
                case "2.0":
                    {
                        loader = new ModelLoaderV2();
                        break;
                    }
                case "3.0": { break; } //Current
                default: { throw new InvalidOperationException("Unhandled version"); }
            }
            return loader;
        }

        private static Model Deserialize<T>(XDocument doc) where T : Model
        {
            Model m = null;
            using (XmlReader xr = doc.CreateReader())
            {
               XmlSerializer xs = new XmlSerializer(typeof(T));
               m = (Model)xs.Deserialize(xr);
               xr.Close();
            }
            return m;
        }
    }

    public interface IModelLoader
    {
        void Populate(ref Model model);
    }

    public class ModelLoaderV1 : IModelLoader
    {
        public void Populate(ref Model model)
        {
            model.City = string.Empty;
            model.Phone = "(000)-000-0000";
        }
    }

    public class ModelLoaderV2 : IModelLoader
    {
        public void Populate(ref Model model)
        {
            model.Phone = "(000)-000-0000";
        }
    }

    public class Model
    {
        [XmlAttribute(AttributeName = "Version")]
        public string Version { get { return "3.0"; } set { } }
        public string Name { get; set; } //V1, V2, V3
        public string City { get; set; } //V2, V3
        public string Phone { get; set; } //V3 only

    }

Depending on your requirements, this could be simplified into a single method on the model (or a model loader).

This could also be used for model validation after deserialization.

Edit: you can serialize using the following code

 private static void Serialize(Model model)
        {
            XmlSerializer xs = new XmlSerializer(typeof(Model));
            FileStream f = File.Create(@"..\..\v1.xml");
            xs.Serialize(f, model);

            f.Close();
        }
Dustin Davis
  • 14,482
  • 13
  • 63
  • 119
  • May be you can provide some sample checking version number and then using startegy pattern – Surjit Samra Nov 18 '11 at 00:16
  • you don't need in most of the cases however you can do it this way also. – vittore Nov 18 '11 at 00:28
  • Can you please write steps how to use it. As with current setup it is coming back with "Unhandled version" for obvious reason as i dont think you have tested it by yourself – Surjit Samra Nov 18 '11 at 17:28
  • Pasted the wrong verion of the model. It should have been a string, not an int. – Dustin Davis Nov 18 '11 at 17:31
  • @DustinDavis Although I can see what you are trying to achieve but can you post your references which suggest this is the best way to go for.I am still not able to find any resources who advocate to control version the way you are suggesting. Here is best practices suggested by Microsoft http://msdn.microsoft.com/en-us/library/ms229752(v=vs.80).aspx – Surjit Samra Nov 19 '11 at 06:11
  • @Surjit You're missing the point. The OP wants to set defaults for newly added members. I'm sorry that you don't understand the code. You can start with these to get up to speed. http://en.wikipedia.org/wiki/Strategy_pattern and http://www.amazon.com/3-0-Design-Patterns-Judith-Bishop/dp/059652773X/ref=sr_1_4?ie=UTF8&qid=1321685995&sr=8-4 – Dustin Davis Nov 19 '11 at 07:02
  • @Surjit Furthermore, I never said this was the best way as there are many other ways he could go such as setting the defaults in the constructor without regard to versioning. Then the deserialize process will populate only elements that it can. No need for added complexity. My solution is a better way to solve the OP's problem compared to the solution you provided which doesn't address the default values issue. Your solution would not work as expected if the serialize process changed the order of the elements, easily done using [XmlElement(Order = 99)] – Dustin Davis Nov 19 '11 at 07:07
  • @DustinDavis may be you need to careful while writing comments like " you don't understand the code" and you should not be forcing patterns just for the sake of it. – Surjit Samra Nov 19 '11 at 07:16
  • @DustinDavis I think you misread my question when I asked to post references I mean is your solution industry recommended,I do not want reference to a desgin pattern book, I know enough desgin patterns to write my own book :). I did like your XmlElement(Order = 99) can you post some samples please. – Surjit Samra Nov 19 '11 at 13:55
3

If you follow this pattern it's fairly straightforward:

  • Handle the serialization/deserialization yourself by implementing ISerializable
  • Use this to serialize both the members of your object and a serialization version number.
  • On the deserialization code, run a switch-case statement against the version number. When you start out, you'll just have one version - the initial deserialization code. As you go forward, you'll stamp a newer version number into newly serialized snapshots.
  • For future versions of your object, always leave the existing deserialization code intact, or modify it to map to members you rename / refactor, and primarily just add a new case statement for the new serialization version.

In this way, you will be able to successfully deserialize previous data even if the serialization snapshot was generated from a previous version of your assembly.

Alex Norcliffe
  • 2,439
  • 2
  • 17
  • 21
  • i think you overcomplicate things, using DataContracts and explicit marking which members are to be serialized, handling versioning of data contracts turns into piece of cake. – vittore Nov 18 '11 at 00:30
2

Use the [System.ComponentModel.DefaultValueAttribute] to define DefaultValues for Serialization.

Example from the MSDN:

private bool myVal=false;

[DefaultValue(false)]
 public bool MyProperty {
    get {
       return myVal;
    }
    set {
       myVal=value;
    }
 }

So if you DeSerialize and the Property is not filled it would use the defaultValue as Value and you can use your old XML to generate the new Object.

If there were Properties removed in the new Version this should go without any problems trough XMLSerialization. (as far as i know)

oberfreak
  • 1,799
  • 13
  • 20
1

You can user ExtendedXmlSerializer. This serializer support deserialization old version of xml. Here is an example of deserialize old version of xml

You can even read different versions of an object from one file.

0

.NET provide quite a lot for serializing/deserializing and versioning.

1) user DataContract / DataMember attributes and DataContractSerializer

2) according to MSDN these changes are breaking

  • Changing the Name or Namespace value of a data contract.
  • Changing the order of data members by using the Order property of the DataMemberAttribute.
  • Renaming a data member.
  • Changing the data contract of a data member.

3) When a type with an extra field is deserialized into a type with a missing field, the extra information is ignored.

4) When a type with a missing field is deserialized into a type with an extra field, the extra field is left at its default value, usually zero or null.

5) Consider using IExtensibleDataObject for versioning

vittore
  • 17,449
  • 6
  • 44
  • 82