简体   繁体   中英

In C# how do I deserialize XML from an older object into the updated object and ignore missing xml elements?

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?

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 在此输入图像描述

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

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();
        }

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.

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)

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.

.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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM