In one of my C# projects I use a WCF data contract serializer for serialization to XML. The framework however consists of multiple extension modules that may be loaded or not, dependent on some startup configuration (I use MEF in case it matters). In the future the list of modules may potentially grow and I fear that this situation may someday pose problems with module-specific data. As I understand I can implement a data contract resolver to bidirectionally help the serializer locate types, but what happens if the project contains data it cannot interpret because the associated module is not loaded?
I am looking for a solution that allows me to preserve existing serialized data in cases where not the full set of modules is loaded (or even available). I think of this as a way to tell the de-serializer "if you don't understand what you get, then don't try to serialize it, but please keep the data somewhere so that you can put it back when serializing the next time". I think my problem is related to round-tripping, but I wasn't very successful (yet) in finding a hint on how to deal with such a case where complex types may be added or removed between serialization actions.
Minimal example:
Suppose I start my application with the optional modules A, B and C and produce the following XML (AData, BData and CData are in a collection and may be all derived from a common base class):
<Project xmlns="http://schemas.datacontract.org/2004/07/TestApplication" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Data>
<ModuleData i:type="AData">
<A>A</A>
</ModuleData>
<ModuleData i:type="BData">
<B>B</B>
</ModuleData>
<ModuleData i:type="CData">
<C>C</C>
</ModuleData>
</Data>
</Project>
In case I skip module C (containing the definition of CData) and load the same project, then the serializer fails because it has no idea how to deal with CData. If I can somehow manage to convince the framework to keep the data and leave it untouched until someone opens the project again with module C, then I win. Of course I could implement dynamic data structures for storing extension data, e.g., key-value trees, but it would be neat to use the existing serialization framework also in extension modules. Any hint on how to achieve this is highly appreciated!
The example code to produce the above output is as follows:
using System;
using System.IO;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace TestApplication
{
// common base class
[DataContract]
public class ModuleData : IExtensibleDataObject
{
public virtual ExtensionDataObject ExtensionData { get; set; }
}
[DataContract]
public class AData : ModuleData
{
[DataMember]
public string A { get; set; }
}
[DataContract]
public class BData : ModuleData
{
[DataMember]
public string B { get; set; }
}
[DataContract]
public class CData : ModuleData
{
[DataMember]
public string C { get; set; }
}
[DataContract]
[KnownType(typeof(AData))]
[KnownType(typeof(BData))]
public class Project
{
[DataMember]
public List<ModuleData> Data { get; set; }
}
class Program
{
static void Main(string[] args)
{
// new project object
var project1 = new Project()
{
Data = new List<ModuleData>()
{
new AData() { A = "A" },
new BData() { B = "B" },
new CData() { C = "C" }
}
};
// serialization; make CData explicitly known to simulate presence of "module C"
var stream = new MemoryStream();
var serializer1 = new DataContractSerializer(typeof(Project), new[] { typeof(CData) });
serializer1.WriteObject(stream, project1);
stream.Position = 0;
var reader = new StreamReader(stream);
Console.WriteLine(reader.ReadToEnd());
// deserialization; skip "module C"
stream.Position = 0;
var serializer2 = new DataContractSerializer(typeof(Project));
var project2 = serializer2.ReadObject(stream) as Project;
}
}
}
I also uploaded a VS2015 solution here.
Your problem is that you have a polymorphic known type hierarchy, and you would like to use the round-tripping mechanism of DataContractSerializer to read and save "unknown" known types, specifically XML elements with an xsi:type type hint referring to a type not currently loaded into your app domain.
Unfortunately, this use case simply isn't implemented by the round-tripping mechanism. That mechanism is designed to cache unknown data members inside an ExtensionData object, provided that the data contract object itself can be successfully deserialized and implements IExtensibleDataObject. Unfortunately, in your situation the data contract object cannot be constructed precisely because the polymorphic subtype is unrecognized; instead the following exception gets thrown:
System.Runtime.Serialization.SerializationException occurred
Message="Error in line 4 position 6. Element
'http://www.Question45412824.com:ModuleData' contains data of the
'http://www.Question45412824.com:CData' data contract. The
deserializer has no knowledge of any type that maps to this contract.
Add the type corresponding to 'CData' to the list of known types - for
example, by using the KnownTypeAttribute attribute or by adding it to
the list of known types passed to DataContractSerializer."
Even if I try to create a custom generic collection marked with [CollectionDataContract] that implements IExtensibleDataObject to cache items with unrecognized contracts, the same exception gets thrown.
One solution is to take advantage of the fact that your problem is slightly less difficult than the round-tripping problem. You (the software architect) actually know all possible polymorphic subtypes. Your software does not, because it isn't always loading the assemblies that contain them. Thus what you can do is load lightweight dummy types instead of the real types when the real types aren't needed. As long as the dummy types implement IExtensibleDataObject and have the same data contract namespace and name and the real types, their data contracts will be interchangeable with the "real" data contracts in polymorphic collections.
Thus, if you define your types as follows, adding a Dummies.CData dummy placeholder:
public static class Namespaces
{
// The data contract namespace for your project.
public const string ProjectNamespace = "http://www.Question45412824.com";
}
// common base class
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class ModuleData : IExtensibleDataObject
{
public ExtensionDataObject ExtensionData { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class AData : ModuleData
{
[DataMember]
public string A { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class BData : ModuleData
{
[DataMember]
public string B { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
[KnownType(typeof(AData))]
[KnownType(typeof(BData))]
public class Project
{
[DataMember]
public List<ModuleData> Data { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class CData : ModuleData
{
[DataMember]
public string C { get; set; }
}
namespace Dummies
{
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class CData : ModuleData
{
}
}
You will be able to deserialize your Project object using either the "real" CData or the "dummy" version, as shown with the test below:
class Program
{
static void Main(string[] args)
{
new TestClass().Test();
}
}
class TestClass
{
public virtual void Test()
{
// new project object
var project1 = new Project()
{
Data = new List<ModuleData>()
{
new AData() { A = "A" },
new BData() { B = "B" },
new CData() { C = "C" }
}
};
// serialization; make CData explicitly known to simulate presence of "module C"
var extraTypes = new[] { typeof(CData) };
var extraTypesDummy = new[] { typeof(Dummies.CData) };
var xml = project1.SerializeXml(extraTypes);
ConsoleAndDebug.WriteLine(xml);
// Demonstrate that the XML can be deserialized with the dummy CData type.
TestDeserialize(project1, xml, extraTypesDummy);
// Demonstrate that the XML can be deserialized with the real CData type.
TestDeserialize(project1, xml, extraTypes);
try
{
// Demonstrate that the XML cannot be deserialized without either the dummy or real type.
TestDeserialize(project1, xml, new Type[0]);
Assert.IsTrue(false);
}
catch (AssertionFailedException ex)
{
Console.WriteLine("Caught unexpected exception: ");
Console.WriteLine(ex);
throw;
}
catch (Exception ex)
{
ConsoleAndDebug.WriteLine(string.Format("Caught expected exception: {0}", ex.Message));
}
}
public void TestDeserialize<TProject>(TProject project, string xml, Type[] extraTypes)
{
TestDeserialize<TProject>(xml, extraTypes);
}
public void TestDeserialize<TProject>(string xml, Type[] extraTypes)
{
var project2 = xml.DeserializeXml<TProject>(extraTypes);
var xml2 = project2.SerializeXml(extraTypes);
ConsoleAndDebug.WriteLine(xml2);
// Assert that the incoming and re-serialized XML are equivalent (no data was lost).
Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
}
}
public static partial class DataContractSerializerHelper
{
public static string SerializeXml<T>(this T obj, Type [] extraTypes)
{
return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), extraTypes));
}
public static string SerializeXml<T>(this T obj, DataContractSerializer serializer)
{
serializer = serializer ?? new DataContractSerializer(obj == null ? typeof(T) : obj.GetType());
using (var textWriter = new StringWriter())
{
var settings = new XmlWriterSettings { Indent = true };
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
{
serializer.WriteObject(xmlWriter, obj);
}
return textWriter.ToString();
}
}
public static T DeserializeXml<T>(this string xml, Type[] extraTypes)
{
return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), extraTypes));
}
public static T DeserializeXml<T>(this string xml, DataContractSerializer serializer)
{
using (var textReader = new StringReader(xml ?? ""))
using (var xmlReader = XmlReader.Create(textReader))
{
return (T)(serializer ?? new DataContractSerializer(typeof(T))).ReadObject(xmlReader);
}
}
}
public static class ConsoleAndDebug
{
public static void WriteLine(object s)
{
Console.WriteLine(s);
Debug.WriteLine(s);
}
}
public class AssertionFailedException : System.Exception
{
public AssertionFailedException() : base() { }
public AssertionFailedException(string s) : base(s) { }
}
public static class Assert
{
public static void IsTrue(bool value)
{
if (value == false)
throw new AssertionFailedException("failed");
}
}
Another solution would be to replace your List<ModuleData> with a custom collection that implements IXmlSerializable and handles the polymorphic serialization entirely manually, caching the XML for unknown polymorphic subtypes in a list of unknown elements. I wouldn't recommend that however since even straightforward implementations of IXmlSerializable can be quite complex, as shown here and, e.g., here.
Following dbc's wonderful suggestion of using dummies to exploit the roundtripping mechanism to do the job, I made the solution more generic by generating the dummy types on the fly as needed.
The core of this solution is the following simple function that internally invokes the C# compiler:
private Type CreateDummyType(string typeName, string typeNamespace)
{
var className = $"DummyClass_{random_.Next()}";
var code = $"[System.Runtime.Serialization.DataContract(Name=\"{typeName}\", Namespace=\"{typeNamespace}\")] public class {className} : ModuleData {{}}";
using (var provider = new CSharpCodeProvider())
{
var parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
parameters.ReferencedAssemblies.Add(GetType().Assembly.Location); // this assembly (for ModuleData)
var results = provider.CompileAssemblyFromSource(parameters, code);
return results.CompiledAssembly.GetType(className);
}
}
I combined this with a DataContractResolver that takes care of any unknown types and generates dummies as needed to preserve their data during subsequent (de)serializations.
For completeness I put the recent iteration of the sample code here:
using System;
using System.IO;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Diagnostics;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
public static class Namespaces
{
public const string BaseNamespace = "http://www.Question45412824.com";
public const string ProjectNamespace = BaseNamespace + "/Project";
public const string ExtensionNamespace = BaseNamespace + "/Extension";
}
// common base class
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class ModuleData : IExtensibleDataObject
{
public ExtensionDataObject ExtensionData { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class AData : ModuleData
{
[DataMember]
public string A { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
public class BData : ModuleData
{
[DataMember]
public string B { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
[KnownType(typeof(AData))]
[KnownType(typeof(BData))]
public class Project
{
[DataMember]
public List<ModuleData> Data { get; set; }
}
[DataContract(Namespace = Namespaces.ProjectNamespace)]
internal class CSubData : ModuleData
{
[DataMember]
public string Name { get; set; }
}
[DataContract(Namespace = Namespaces.ExtensionNamespace)]
public class CData : ModuleData
{
[DataMember]
public ModuleData C { get; set; }
}
class Program
{
static void Main(string[] args)
{
new TestClass().Test();
}
}
class TestClass
{
public virtual void Test()
{
// new project object
var project1 = new Project()
{
Data = new List<ModuleData>()
{
new AData() { A = "A" },
new BData() { B = "B" },
new CData() { C = new CSubData() { Name = "C" } }
}
};
// serialization; make CData explicitly known to simulate presence of "module C"
var extraTypes = new[] { typeof(CData), typeof(CSubData) };
ConsoleAndDebug.WriteLine("\n== Serialization with all types known ==");
var xml = project1.SerializeXml(extraTypes);
ConsoleAndDebug.WriteLine(xml);
ConsoleAndDebug.WriteLine("\n== Deserialization and subsequent serialization WITH generic resolver and unknown types ==");
TestDeserialize(project1, xml, new GenericDataContractResolver());
ConsoleAndDebug.WriteLine("\n== Deserialization and subsequent serialization WITHOUT generic resolver and unknown types ==");
try
{
// Demonstrate that the XML cannot be deserialized without the generic resolver.
TestDeserialize(project1, xml, new Type[0]);
Assert.IsTrue(false);
}
catch (AssertionFailedException ex)
{
Console.WriteLine("Caught unexpected exception: ");
Console.WriteLine(ex);
throw;
}
catch (Exception ex)
{
ConsoleAndDebug.WriteLine(string.Format("Caught expected exception: {0}", ex.Message));
}
}
public void TestDeserialize<TProject>(TProject project, string xml, Type[] extraTypes)
{
TestDeserialize<TProject>(xml, extraTypes);
}
public void TestDeserialize<TProject>(string xml, Type[] extraTypes)
{
var project2 = xml.DeserializeXml<TProject>(extraTypes);
var xml2 = project2.SerializeXml(extraTypes);
ConsoleAndDebug.WriteLine(xml2);
// Assert that the incoming and re-serialized XML are equivalent (no data was lost).
Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
}
public void TestDeserialize<TProject>(TProject project, string xml, DataContractResolver resolver)
{
TestDeserialize<TProject>(xml, resolver);
}
public void TestDeserialize<TProject>(string xml, DataContractResolver resolver)
{
var project2 = xml.DeserializeXml<TProject>(resolver);
var xml2 = project2.SerializeXml(resolver);
ConsoleAndDebug.WriteLine(xml2);
// Assert that the incoming and re-serialized XML are equivalent (no data was lost).
Assert.IsTrue(XNode.DeepEquals(XElement.Parse(xml), XElement.Parse(xml2)));
}
}
public static partial class DataContractSerializerHelper
{
public static string SerializeXml<T>(this T obj, Type[] extraTypes)
{
return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), extraTypes));
}
public static string SerializeXml<T>(this T obj, DataContractResolver resolver)
{
return obj.SerializeXml(new DataContractSerializer(obj == null ? typeof(T) : obj.GetType(), null, int.MaxValue, false, false, null, resolver));
}
public static string SerializeXml<T>(this T obj, DataContractSerializer serializer)
{
serializer = serializer ?? new DataContractSerializer(obj == null ? typeof(T) : obj.GetType());
using (var textWriter = new StringWriter())
{
var settings = new XmlWriterSettings { Indent = true };
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
{
serializer.WriteObject(xmlWriter, obj);
}
return textWriter.ToString();
}
}
public static T DeserializeXml<T>(this string xml, DataContractResolver resolver)
{
return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), null, int.MaxValue, false, false, null, resolver));
}
public static T DeserializeXml<T>(this string xml, Type[] extraTypes)
{
return xml.DeserializeXml<T>(new DataContractSerializer(typeof(T), extraTypes));
}
public static T DeserializeXml<T>(this string xml, DataContractSerializer serializer)
{
using (var textReader = new StringReader(xml ?? ""))
using (var xmlReader = XmlReader.Create(textReader))
{
return (T)(serializer ?? new DataContractSerializer(typeof(T))).ReadObject(xmlReader);
}
}
}
public static class ConsoleAndDebug
{
public static void WriteLine(object s)
{
Console.WriteLine(s);
Debug.WriteLine(s);
}
}
public class AssertionFailedException : System.Exception
{
public AssertionFailedException() : base() { }
public AssertionFailedException(string s) : base(s) { }
}
public static class Assert
{
public static void IsTrue(bool value)
{
if (value == false)
throw new AssertionFailedException("failed");
}
}
class GenericDataContractResolver : DataContractResolver
{
private static readonly Random random_ = new Random();
private static readonly Dictionary<Tuple<string, string>, Type> toType_ = new Dictionary<Tuple<string, string>, Type>();
private static readonly Dictionary<Type, Tuple<string, string>> fromType_ = new Dictionary<Type, Tuple<string, string>>();
private Type CreateDummyType(string typeName, string typeNamespace)
{
var className = $"DummyClass_{random_.Next()}";
var code = $"[System.Runtime.Serialization.DataContract(Name=\"{typeName}\", Namespace=\"{typeNamespace}\")] public class {className} : ModuleData {{}}";
using (var provider = new CSharpCodeProvider())
{
var parameters = new CompilerParameters();
parameters.ReferencedAssemblies.Add("System.Runtime.Serialization.dll");
parameters.ReferencedAssemblies.Add(GetType().Assembly.Location); // this assembly (for ModuleData)
var results = provider.CompileAssemblyFromSource(parameters, code);
return results.CompiledAssembly.GetType(className);
}
}
// Used at deserialization; allows users to map xsi:type name to any Type
public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
{
var type = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);
// resolve all unknown extension datasets; all other should be explicitly known.
if (type == null && declaredType == typeof(ModuleData) && typeNamespace == Namespaces.ExtensionNamespace)
{
// if we already have this type cached, then return the cached one
var typeNameAndNamespace = new Tuple<string, string>(typeName, typeNamespace);
if (toType_.TryGetValue(typeNameAndNamespace, out type))
return type;
// else compile the dummy type and remember it in the cache
type = CreateDummyType(typeName, typeNamespace);
toType_.Add(typeNameAndNamespace, type);
fromType_.Add(type, typeNameAndNamespace);
}
return type;
}
// Used at serialization; maps any Type to a new xsi:type representation
public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)
{
if (knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace))
return true; // known type
// is the type one of our cached dummies?
var typeNameAndNamespace = default(Tuple<string, string>);
if (declaredType == typeof(ModuleData) && fromType_.TryGetValue(type, out typeNameAndNamespace))
{
typeName = new XmlDictionaryString(XmlDictionary.Empty, typeNameAndNamespace.Item1, 0);
typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, typeNameAndNamespace.Item2, 0);
return true; // dummy type
}
return false; // unknown type
}
}
Related
I've got a simple .Net framework C# console app that serializes a a class that is of a derived type, where a property is also of a derived type.
The derived classes have names that are the same as the base class, but are in a different namespace to prevent them from clashing. It seems though that the reflection the XmlSerializer uses does not work with this too well. Maybe there is some way of wrangling the attributes that I can still end up with the base class using pretty names (as it will be a DLL interface when used) and the XML also using pretty names (as it will be human editable)... pretty names for the derived classes are not required (though would be a bonus).
The XML would hopefully look like:
<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Details>
<Detail>
<Description>bald</Description>
</Detail>
<Detail>
<Description>red tie</Description>
</Detail>
</Details>
</Person>
But the closest I can get without exceptions is where the <Detail> elements are
<Detail xsi:type="DerivedDetail"> ... </Detail>
Having to add this xs:type attribute is not the best for human-editable XML.
This is achieved with the below C# code. If I remove the marked XmlType attribute then the element should serialize without the xsi:type attribute, but instead I get an exception:
InvalidOperationException: Types 'Test.Detail' and 'Test.Xml.Detail' both use the XML type name, 'Detail', from namespace ''. Use XML attributes to specify a unique XML name and/or namespace for the type.
I tried marking the derived Xml.Detail class as an anonymous XML type but then the exception reads:
InvalidOperationException: Cannot include anonymous type 'Test.Xml.Detail'.
I have read many similar questions but have not encountered anything that solves this just yet.
In this code below Person is an abstract class that has a property that is an array of the abstract type Detail. These types are derived by Xml.Person and Xml.Detail respectively. The program creates a test Xml.Person object and attempts to serialize it:
using System;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace Test
{
class Program
{
static void Main(string[] args)
{
// Create test details array
var TestDetails = new Xml.Detail[]
{
new Xml.Detail
{
Description = "bald"
},
new Xml.Detail
{
Description = "red tie"
}
};
// create test person object that holds details array
var TestBar = new Xml.Person()
{
Details = TestDetails
};
// serialize the person object
var s = new Xml.Serializer();
var TestOutput = s.Serialize(TestBar);
Console.WriteLine(TestOutput);
}
}
// base classes
public abstract class Person
{
public abstract Detail[] Details { get; set; }
}
public abstract class Detail
{
public abstract string Description { get; set; }
}
namespace Xml
{
// derived classes
[Serializable]
[XmlType(AnonymousType = true)]
[XmlRoot(IsNullable = false)]
public class Person : Test.Person
{
[XmlArrayItem("Detail", typeof(Detail))]
[XmlArray(IsNullable = false)]
public override Test.Detail[] Details { get; set; }
}
// This attribute makes serialization work but also adds the xsi:type attribute
[XmlType("DerivedDetail")]
[Serializable]
public class Detail : Test.Detail
{
public override string Description { get; set; }
}
// class that does serializing work
public class Serializer
{
private static XmlSerializer PersonSerializer =
new XmlSerializer(typeof(Person), new Type[] { typeof(Detail) });
public string Serialize(Test.Person person)
{
string Output = null;
var Stream = new MemoryStream();
var Encoding = new UTF8Encoding(false, true);
using (var Writer = new XmlTextWriter(Stream, Encoding))
{
Writer.Formatting = Formatting.Indented;
PersonSerializer.Serialize(Writer, person);
Output = Encoding.GetString(Stream.ToArray());
}
Stream.Dispose();
return Output;
}
}
}
}
Not sure why you're using base classes instead of interfaces when you don't have any member fields. Regardless, I assumed you wanted Xml.Person to be a concrete instantiation of abstract Person or any classes derived from abstract Person without decorating abstract Person with XML attributes. I accomplished this by forcing abstract Person to become a concrete instantiation of Xml.Person before serializing it. Please replace XmlSerializationProject with Test.
using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace XmlSerializationProject
{
class Program
{
static void Main(string[] args)
{
// Create test details array
var TestDetails = new Xml.Detail[]
{
new Xml.Detail
{
Description = "bald"
},
new Xml.Detail
{
Description = "red tie"
}
};
// create test person object that holds details array
var TestBar = new Xml.Person()
{
Details = TestDetails
};
// serialize the person object
var s = new Xml.Serializer();
var TestOutput = s.Serialize(TestBar);
Console.WriteLine(TestOutput);
Console.ReadKey();
}
}
// base classes
public abstract class Person
{
public abstract Detail[] Details { get; set; }
}
public abstract class Detail
{
public abstract string Description { get; set; }
}
namespace Xml
{
[Serializable]
[XmlType(AnonymousType = true)]
[XmlRoot(IsNullable = false)]
public class Person : XmlSerializationProject.Person
{
public Person()
{ }
public Person(XmlSerializationProject.Person person)
{
// Deep copy
if (person.Details == null) return;
this.Details = new Detail[person.Details.Length];
for (int i = 0; i < person.Details.Length; i++)
{
this.Details[i] = new Detail { Description = person.Details[i].Description };
}
}
[XmlArray(ElementName = "Details")]
[XmlArrayItem("Detail", typeof(Detail))]
[XmlArrayItem("ODetail", typeof(XmlSerializationProject.Detail))]
public override XmlSerializationProject.Detail[] Details
{
get;
set;
}
}
[Serializable]
public class Detail : XmlSerializationProject.Detail
{
public override string Description { get; set; }
}
// class that does serializing work
public class Serializer
{
private static readonly XmlSerializer PersonSerializer;
private static Serializer()
{
var xmlAttributeOverrides = new XmlAttributeOverrides();
// Change original "Detail" class's element name to "AbstractDetail"
var xmlAttributesOriginalDetail = new XmlAttributes();
xmlAttributesOriginalDetail.XmlType = new XmlTypeAttribute() { TypeName = "AbstractDetail" };
xmlAttributeOverrides.Add(typeof(XmlSerializationProject.Detail), xmlAttributesOriginalDetail);
// Ignore Person.Details array
var xmlAttributesOriginalDetailsArray = new XmlAttributes();
xmlAttributesOriginalDetailsArray.XmlIgnore = true;
xmlAttributeOverrides.Add(typeof(XmlSerializationProject.Person), "Details", xmlAttributesOriginalDetailsArray);
PersonSerializer = new XmlSerializer(
typeof(Person), xmlAttributeOverrides, new Type[] { typeof(Detail) }, new XmlRootAttribute(), "default");
}
public string Serialize(XmlSerializationProject.Person person)
{
return Serialize(new Person(person));
}
public string Serialize(Person person)
{
string Output = null;
var Stream = new MemoryStream();
var Encoding = new UTF8Encoding(false, true);
using (var Writer = new XmlTextWriter(Stream, Encoding))
{
Writer.Formatting = Formatting.Indented;
PersonSerializer.Serialize(Writer, person);
Output = Encoding.GetString(Stream.ToArray());
}
Stream.Dispose();
return Output;
}
}
}
}
Given following json result:
The default json result has a known set of fields:
{
"id": "7908",
"name": "product name"
}
But can be extended with additional fields (in this example _unknown_field_name_1 and _unknown_field_name_2) of which the names are not known when requesting the result.
{
"id": "7908",
"name": "product name",
"_unknown_field_name_1": "some value",
"_unknown_field_name_2": "some value"
}
I would like the json result to be serialized and deserialized to and from a class with properties for the known fields and map the unknown fields (for which there are no properties) to a property (or multiple properties) like a dictionary so they can be accessed and modified.
public class Product
{
public string id { get; set; }
public string name { get; set; }
public Dictionary<string, string> fields { get; set; }
}
I think I need a way to plug into a json serializer and do the mapping for the missing members myself (both for serialize and deserialize).
I have been looking at various possibilities:
json.net and custom contract resolvers (can't figure out how to do it)
datacontract serializer (can only override onserialized, onserializing)
serialize to dynamic and do custom mapping (this might work, but seems a lot of work)
let product inheriting from DynamicObject (serializers work with reflection and do not invoke the trygetmember and trysetmember methods)
I'm using restsharp, but any serializer can be plugged in.
Oh, and I cannot change the json result, and this or this didn't help me either.
Update:
This looks more like it: http://geekswithblogs.net/DavidHoerster/archive/2011/07/26/json.net-custom-convertersndasha-quick-tour.aspx
An even easier option to tackling this problem would be to use the JsonExtensionDataAttribute from JSON .NET
public class MyClass
{
// known field
public decimal TaxRate { get; set; }
// extra fields
[JsonExtensionData]
private IDictionary<string, JToken> _extraStuff;
}
There's a sample of this on the project blog here
UPDATE Please note this requires JSON .NET v5 release 5 and above
See https://gist.github.com/LodewijkSioen/5101814
What you were looking for was a custom JsonConverter
This is a way you could solve it, although I don't like it that much. I solved it using Newton/JSON.Net. I suppose you could use the JsonConverter for deserialization aswell.
private const string Json = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";
[TestMethod]
public void TestDeserializeUnknownMembers()
{
var #object = JObject.Parse(Json);
var serializer = new Newtonsoft.Json.JsonSerializer();
serializer.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
serializer.Error += (sender, eventArgs) =>
{
var contract = eventArgs.CurrentObject as Contract ?? new Contract();
contract.UnknownValues.Add(eventArgs.ErrorContext.Member.ToString(), #object[eventArgs.ErrorContext.Member.ToString()].Value<string>());
eventArgs.ErrorContext.Handled = true;
};
using (MemoryStream memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(Json)))
using (StreamReader streamReader = new StreamReader(memoryStream))
using (JsonReader jsonReader = new JsonTextReader(streamReader))
{
var result = serializer.Deserialize<Contract>(jsonReader);
Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_1"));
Assert.IsTrue(result.UnknownValues.ContainsKey("_unknown_field_name_2"));
}
}
[TestMethod]
public void TestSerializeUnknownMembers()
{
var deserializedObject = new Contract
{
id = 7908,
name = "product name",
UnknownValues = new Dictionary<string, string>
{
{"_unknown_field_name_1", "some value"},
{"_unknown_field_name_2", "some value"}
}
};
var json = JsonConvert.SerializeObject(deserializedObject, new DictionaryConverter());
Console.WriteLine(Json);
Console.WriteLine(json);
Assert.AreEqual(Json, json);
}
}
class DictionaryConverter : JsonConverter
{
public DictionaryConverter()
{
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Contract);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = value as Contract;
var json = JsonConvert.SerializeObject(value);
var dictArray = String.Join(",", contract.UnknownValues.Select(pair => "\"" + pair.Key + "\":\"" + pair.Value + "\""));
json = json.Substring(0, json.Length - 1) + "," + dictArray + "}";
writer.WriteRaw(json);
}
}
class Contract
{
public Contract()
{
UnknownValues = new Dictionary<string, string>();
}
public int id { get; set; }
public string name { get; set; }
[JsonIgnore]
public Dictionary<string, string> UnknownValues { get; set; }
}
}
I thought I'd throw my hat in the ring since I had a similar problem recently. Here's an example of the JSON I wanted to deserialize:
{
"agencyId": "agency1",
"overrides": {
"assumption.discount.rates": "value: 0.07",
".plan": {
"plan1": {
"assumption.payroll.growth": "value: 0.03",
"provision.eeContrib.rate": "value: 0.35"
},
"plan2": {
".classAndTier": {
"misc:tier1": {
"provision.eeContrib.rate": "value: 0.4"
},
"misc:tier2": {
"provision.eeContrib.rate": "value: 0.375"
}
}
}
}
}
}
This is for a system where overrides apply at different levels and are inherited down the tree. In any case, the data model I wanted was something that would allow me to have a property bag with these special inheritance rules also supplied.
What I ended up with was the following:
public class TestDataModel
{
public string AgencyId;
public int Years;
public PropertyBagModel Overrides;
}
public class ParticipantFilterModel
{
public string[] ClassAndTier;
public string[] BargainingUnit;
public string[] Department;
}
public class PropertyBagModel
{
[JsonExtensionData]
private readonly Dictionary<string, JToken> _extensionData = new Dictionary<string, JToken>();
[JsonIgnore]
public readonly Dictionary<string, string> Values = new Dictionary<string, string>();
[JsonProperty(".plan", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, PropertyBagModel> ByPlan;
[JsonProperty(".classAndTier", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, PropertyBagModel> ByClassAndTier;
[JsonProperty(".bargainingUnit", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary<string, PropertyBagModel> ByBarginingUnit;
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
foreach (var kvp in Values)
_extensionData.Add(kvp.Key, kvp.Value);
}
[OnSerialized]
private void OnSerialized(StreamingContext context)
{
_extensionData.Clear();
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
Values.Clear();
foreach (var kvp in _extensionData.Where(x => x.Value.Type == JTokenType.String))
Values.Add(kvp.Key, kvp.Value.Value<string>());
_extensionData.Clear();
}
}
The basic idea is this:
The PropertyBagModel on deserialization by JSON.NET has the ByPlan, ByClassAndTier, etc. fields populated and also has the private _extensionData field populated.
Then JSON.NET calls the private OnDeserialized() method and that will move the data from _extensionData to Values as appropriate (or drop it on the floor otherwise - presumably you could log this if it was something you wanted to know). We then remove the extra gunk from _extensionData so it doesn't consume memory.
On serialization, the OnSerializing method gets calls where we move stuff into _extensionData so it gets saved.
When serialization has finished, OnSerialized gets called and we remove the extra stuff from _extensionData.
We could further delete and recreate the _extensionData Dictionary when needed but I didn't see a real value in this as I'm not using tons of these objects. To do this we'd just create on OnSerializing and delete on OnSerialized. On OnDeserializing, instead of clearing, we could free it.
I was looking into a similar issue and found this post.
Here is a way to do it using reflection.
To make it more generic, one should check the type of the property instead of simply using ToString() in propertyInfo.SetValue, unless OFC all the actual properties are strings.
Also, lowercase property names is not standard in C# but given that GetProperty is case sensitive there are few other options.
public class Product
{
private Type _type;
public Product()
{
fields = new Dictionary<string, object>();
_type = GetType();
}
public string id { get; set; }
public string name { get; set; }
public Dictionary<string, object> fields { get; set; }
public void SetProperty(string key, object value)
{
var propertyInfo = _type.GetProperty(key);
if (null == propertyInfo)
{
fields.Add(key,value);
return;
}
propertyInfo.SetValue(this, value.ToString());
}
}
...
private const string JsonTest = "{\"id\":7908,\"name\":\"product name\",\"_unknown_field_name_1\":\"some value\",\"_unknown_field_name_2\":\"some value\"}";
var product = new Product();
var data = JObject.Parse(JsonTest);
foreach (var item in data)
{
product.SetProperty(item.Key, item.Value);
}
I have a XML document provided by client applications to my C# application. This is how a client sends the XML file:
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<parentId>2380983</parentId>
<!-- more elements -->
</SomeAccount>
And a C# class that supports the XML deserialization:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
But there are some clients whose system send the XML in this way (note the upper case in LeParentId):
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<LeParentId>2380983</LeParentId>
<!-- similar for the other elements -->
</SomeAccount>
How can I make this field (and others) to support both XML names parentId and LeParentId?
This is the method I'm currently using for XML deserialization:
public sealed class XmlSerializationUtil
{
public static T Deserialize<T>(string xml)
{
if (xml == null)
return default(T);
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringReader stringReader = new StringReader(xml);
return (T)serializer.Deserialize(stringReader);
}
}
I tried to add [XmlElement] twice in the field, one per element name, but that didn't work.
Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):
public class XmlSynonymDeserializer : XmlSerializer
{
public class SynonymsAttribute : Attribute
{
public readonly ISet<string> Names;
public SynonymsAttribute(params string[] names)
{
this.Names = new HashSet<string>(names);
}
public static MemberInfo GetMember(object obj, string name)
{
Type type = obj.GetType();
var result = type.GetProperty(name);
if (result != null)
return result;
foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
return member;
return null;
}
}
public XmlSynonymDeserializer(Type type)
: base(type)
{
this.UnknownElement += this.SynonymHandler;
}
public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
: base(type, root)
{
this.UnknownElement += this.SynonymHandler;
}
protected void SynonymHandler(object sender, XmlElementEventArgs e)
{
var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
Type memberType;
if (member != null && member is FieldInfo)
memberType = ((FieldInfo)member).FieldType;
else if (member != null && member is PropertyInfo)
memberType = ((PropertyInfo)member).PropertyType;
else
return;
if (member != null)
{
object value;
XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
value = serializer.Deserialize(reader);
if (member is FieldInfo)
((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
else if (member is PropertyInfo)
((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
}
}
}
And now the actual code of the class would be:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
[XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
public long ParentId { get; set; }
//rest of fields...
}
To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.
Known limitations:
This implementation supports only elements with multiple names; extending it for attributes should be trivial
Support for handling of properties/fields in cases where the entities inherit from one another is not tested
This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)
I know this is an old post, but maybe this will help anyone else having the same problem.
What you could use for this problem is XmlChoiceIdentifier.
[XmlRoot]
public class SomeAccount
{
[XmlIgnore]
public ItemChoiceType EnumType;
[XmlChoiceIdentifier("EnumType")]
[XmlElement("LeParentId")]
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
[XmlType(IncludeInSchema = false)]
public enum ItemChoiceType
{
LeParentId,
parentId
}
Now if you have a new xml version and a new XmlElement name you just add that name to the ItemChoiceType enum and a new XmlElement to the property.
If you need only one more name, here is a quick (and rather ugly) solution that we deployed in several cases in my work when we had only to read XMLs (this will be problematic for serializing back to an XML), because it's the simplest and easiest to understand:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
[XmlElement("LeParentId")]
public long LeParentId { get { return this.ParentId; } set { this.ParentId = value; } }
//rest of fields...
}
I have to serialize using the folowing code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
namespace MyExample
{
class Program
{
static void Main(string[] args)
{
MyXmlDocument document = new MyXmlDocument();
document.MyExample.NodeA.value = "Value To Node A";
document.MyExample.NodeB.value = "Value To Node B";
document.MyExample.NodeC.value = 1234.567;
document.WriteToXml(#"C:\Users\E9JR\Desktop\mydocument.xml");
Console.Write("> Done!");
Console.ReadKey();
}
}
[XmlRoot(ElementName="xmlExample",IsNullable=false)]
public class XmlExample
{
private NodeA_Elem _nodea;
[XmlElement()]
public NodeA_Elem NodeA
{
get
{
return _nodea;
}
set
{
_nodea = value;
}
}
public bool ShouldSerializeNodeA()
{
return !String.IsNullOrEmpty(_nodea.value);
}
private NodeB_Elem _nodeb;
[XmlElement(ElementName = "NodeB", IsNullable = false)]
public NodeB_Elem NodeB
{
get
{
return _nodeb;
}
set
{
_nodeb = value;
}
}
public bool ShouldSerializeNodeB()
{
return !String.IsNullOrEmpty(_nodeb.value);
}
private NodeC_Elem _nodec;
[XmlElement(ElementName = "NodeC",IsNullable=false)]
public NodeC_Elem NodeC
{
get
{
return _nodec;
}
set
{
_nodec = value;
}
}
public bool ShouldSerializeNodeC()
{
return _nodec.value.HasValue;
}
public XmlExample()
{
_nodea = new NodeA_Elem();
_nodeb = new NodeB_Elem();
_nodec = new NodeC_Elem();
}
}
public class NodeA_Elem
{
[XmlText()]
public string value { get; set; }
}
public class NodeB_Elem
{
[XmlText()]
public string value { get; set; }
}
public class NodeC_Elem
{
[XmlText()]
public double? value { get; set; }
}
public class MyXmlDocument
{
private XmlExample _myexample;
public XmlExample MyExample
{
get
{
return _myexample;
}
set
{
_myexample = value;
}
}
public void WriteToXml(string path)
{
XmlSerializer serializer = new XmlSerializer(typeof(XmlExample));
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.Encoding = Encoding.Unicode;
StringWriter txtwriter = new StringWriter();
XmlWriter xmlwtr = XmlWriter.Create(txtwriter, settings);
serializer.Serialize(xmlwtr, MyExample);
StreamWriter writer = new StreamWriter(path);
writer.Write(txtwriter.ToString());
writer.Close();
}
public void ReadXml(string path)
{
XmlSerializer serializer = new XmlSerializer(typeof(XmlExample));
StreamReader reader = new StreamReader(path);
MyExample = (XmlExample)serializer.Deserialize(reader);
}
public MyXmlDocument()
{
_myexample = new XmlExample();
}
}
}
I'm trying to serialize NodeC using as text for the node the value property, which is a double, but it's not working, even using the ShouldSerialize pattern to avoid serialize empty nodes. NodeA and NodeB is working fine. I need help for NodeC.
You can't serialize a nullable double as XmlText. If you look at the full text of the System.InvalidOperationException you are getting, you will see something like:
InnerException: System.InvalidOperationException
Message="Cannot serialize member 'value' of type System.Nullable`1[System.Double]. XmlAttribute/XmlText cannot be used to encode complex types."
Source="System.Xml"
StackTrace:
at System.Xml.Serialization.XmlReflectionImporter.ImportAccessorMapping(MemberMapping accessor, FieldModel model, XmlAttributes a, String ns, Type choiceIdentifierType, Boolean rpc, Boolean openModel, RecursionLimiter limiter)
at System.Xml.Serialization.XmlReflectionImporter.ImportFieldMapping(StructModel parent, FieldModel model, XmlAttributes a, String ns, RecursionLimiter limiter)
at System.Xml.Serialization.XmlReflectionImporter.InitializeStructMembers(StructMapping mapping, StructModel model, Boolean openModel, String typeName, RecursionLimiter limiter)
That message is self explanatory. Confirmation from the documentation for XmlTextAttribute:
You can apply the XmlTextAttribute to public fields and public read/write properties that return primitive and enumeration types.
You can apply the XmlTextAttribute to a field or property that returns an array of strings. You can also apply the attribute to an array of type Object but you must set the Type property to string. In that case, any strings inserted into the array are serialized as XML text.
The XmlTextAttribute can also be applied to a field that returns an XmlNode or an array of XmlNode objects.
To understand why ShouldSerializeXXX() doesn't help here, you should understand that XmlSerializer works as follows:
The first time you serialize a type, the XmlSerializer constructor internally writes run-time c# code to serialize and deserialize instances of the type and all referenced types using reflection, then compiles the code and loads the resulting DLL into memory.
Subsequently, serialization and deserialization of class instances are performed by the previously created dynamic DLL.
But step 1 does not have access to the instance of the class. It creates its dynamic library based purely on type information. And, from the type information, there is no way to infer that the relevant ShouldSerializeXXX() method will return false when the double? value is null. Thus, dynamic code generation aborts, because code to write a nullable double as XmlText can't be generated.
As a workaround, you could make a string property that represents the double:
public class NodeC_Elem
{
[XmlIgnore]
public double? value { get; set; }
[XmlText]
public string StringValue
{
get
{
if (value == null)
return null;
return XmlConvert.ToString(value.Value);
}
set
{
if (value == null)
{
this.value = null;
return;
}
this.value = XmlConvert.ToDouble(value);
}
}
}
I have a XML document provided by client applications to my C# application. This is how a client sends the XML file:
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<parentId>2380983</parentId>
<!-- more elements -->
</SomeAccount>
And a C# class that supports the XML deserialization:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
But there are some clients whose system send the XML in this way (note the upper case in LeParentId):
<?xml version="1.0" encoding="utf-8"?>
<SomeAccount>
<LeParentId>2380983</LeParentId>
<!-- similar for the other elements -->
</SomeAccount>
How can I make this field (and others) to support both XML names parentId and LeParentId?
This is the method I'm currently using for XML deserialization:
public sealed class XmlSerializationUtil
{
public static T Deserialize<T>(string xml)
{
if (xml == null)
return default(T);
XmlSerializer serializer = new XmlSerializer(typeof(T));
StringReader stringReader = new StringReader(xml);
return (T)serializer.Deserialize(stringReader);
}
}
I tried to add [XmlElement] twice in the field, one per element name, but that didn't work.
Take 2 - let's implement this ourselves using the unknown element handling event (see the comments below for some limitations though):
public class XmlSynonymDeserializer : XmlSerializer
{
public class SynonymsAttribute : Attribute
{
public readonly ISet<string> Names;
public SynonymsAttribute(params string[] names)
{
this.Names = new HashSet<string>(names);
}
public static MemberInfo GetMember(object obj, string name)
{
Type type = obj.GetType();
var result = type.GetProperty(name);
if (result != null)
return result;
foreach (MemberInfo member in type.GetProperties().Cast<MemberInfo>().Union(type.GetFields()))
foreach (var attr in member.GetCustomAttributes(typeof(SynonymsAttribute), true))
if (attr is SynonymsAttribute && ((SynonymsAttribute)attr).Names.Contains(name))
return member;
return null;
}
}
public XmlSynonymDeserializer(Type type)
: base(type)
{
this.UnknownElement += this.SynonymHandler;
}
public XmlSynonymDeserializer(Type type, XmlRootAttribute root)
: base(type, root)
{
this.UnknownElement += this.SynonymHandler;
}
protected void SynonymHandler(object sender, XmlElementEventArgs e)
{
var member = SynonymsAttribute.GetMember(e.ObjectBeingDeserialized, e.Element.Name);
Type memberType;
if (member != null && member is FieldInfo)
memberType = ((FieldInfo)member).FieldType;
else if (member != null && member is PropertyInfo)
memberType = ((PropertyInfo)member).PropertyType;
else
return;
if (member != null)
{
object value;
XmlSynonymDeserializer serializer = new XmlSynonymDeserializer(memberType, new XmlRootAttribute(e.Element.Name));
using (System.IO.StringReader reader = new System.IO.StringReader(e.Element.OuterXml))
value = serializer.Deserialize(reader);
if (member is FieldInfo)
((FieldInfo)member).SetValue(e.ObjectBeingDeserialized, value);
else if (member is PropertyInfo)
((PropertyInfo)member).SetValue(e.ObjectBeingDeserialized, value);
}
}
}
And now the actual code of the class would be:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
[XmlSynonymDeserializer.Synonyms("LeParentId", "AnotherGreatName")]
public long ParentId { get; set; }
//rest of fields...
}
To deserialize, simply use XmlSynonymDeserializer instead of the regular XmlSerializer. This should work for most of the basic needs.
Known limitations:
This implementation supports only elements with multiple names; extending it for attributes should be trivial
Support for handling of properties/fields in cases where the entities inherit from one another is not tested
This implementation does not check for programming bugs (having the attribute on read-only/constant field/properties, multiple members with the same synonyms and so on)
I know this is an old post, but maybe this will help anyone else having the same problem.
What you could use for this problem is XmlChoiceIdentifier.
[XmlRoot]
public class SomeAccount
{
[XmlIgnore]
public ItemChoiceType EnumType;
[XmlChoiceIdentifier("EnumType")]
[XmlElement("LeParentId")]
[XmlElement("parentId")]
public long ParentId { get; set; }
//rest of fields...
}
[XmlType(IncludeInSchema = false)]
public enum ItemChoiceType
{
LeParentId,
parentId
}
Now if you have a new xml version and a new XmlElement name you just add that name to the ItemChoiceType enum and a new XmlElement to the property.
If you need only one more name, here is a quick (and rather ugly) solution that we deployed in several cases in my work when we had only to read XMLs (this will be problematic for serializing back to an XML), because it's the simplest and easiest to understand:
[XmlRoot]
public class SomeAccount
{
[XmlElement("parentId")]
public long ParentId { get; set; }
[XmlElement("LeParentId")]
public long LeParentId { get { return this.ParentId; } set { this.ParentId = value; } }
//rest of fields...
}