Xml.Serialization object T with attribute value - c#

i am using Xml Attributes on my Models to handle serializing my Models.
the base class is:
public class RequestRoot<T>
{
[XmlElement("Action")]
public T ActionNode { get; set; }
}
ActionNode is of type T, and can be anything from a string to a collection of complex objects.
example:
<?xml version="1.0" encoding="UTF-8"?>
<RequestRoot>
<Action Type="TypeValue A">
<SomeData>Some data</SomeData>
</Action>
</RequestRoot>
<?xml version="1.0" encoding="UTF-8"?>
<RequestRoot>
<Action Type="TypeValue B">
<MyObjects>
<MyObject>
<ObjectValue1>Object Value 1-1</ObjectValue1>
<ObjectValue2>Object Value 2-1</ObjectValue2>
</MyObject>
<MyObject>
<ObjectValue1>Object Value 1-2</ObjectValue1>
<ObjectValue2>Object Value 2-2</ObjectValue2>
</MyObject>
</MyObjects>
</Action>
</RequestRoot>
my question is this: is it possible to use Xml Attributes on my Models to write Type="TypeValue A" or Type="TypeValue B" depending upon what T is?
if not, what alternatives do i have?

There is no way to do this with XmlSerializer out of the box. That's because your RequestRoot class is generic, and XmlSerializer determines the type of object to create based on the XML element name and, possibly, the "xsi:type" attribute. Your type information, however, is embedded in a sub-element Action of the root element which is not accessible at the time the root must needs be allocated.
What you will have to do is to read and write the RequestRoot wrapper manually, then use XmlSerializer for the contents. For instance:
public abstract class RequestRootBase
{
[XmlIgnore]
public abstract Type RequestType { get; }
[XmlIgnore]
public abstract Object RequestObject { get; }
}
public class RequestRoot<T> : RequestRootBase
{
public RequestRoot() { }
public RequestRoot(T ActionNode) { this.ActionNode = ActionNode; }
[XmlElement("Action")]
public T ActionNode { get; set; }
public override Type RequestType
{
get { return typeof(T); }
}
public override object RequestObject
{
get { return ActionNode; }
}
}
public static class RequestRootHelper
{
public static RequestRootBase CreateBase(object action)
{
if (action == null)
throw new ArgumentNullException();
var type = action.GetType();
return (RequestRootBase)Activator.CreateInstance(typeof(RequestRoot<>).MakeGenericType(type), new [] { action });
}
public static RequestRoot<T> Create<T>(T action)
{
return new RequestRoot<T> { ActionNode = action };
}
}
public abstract class RequestRootXmlSerializerBase
{
const string RequestRootElementName = "RequestRoot";
const string ActionElementName = "Action";
const string TypeAttributeName = "Type";
protected abstract Type BindToType(string name);
protected abstract string BindToName(Type type);
static string DefaultRootXmlElementNamespace(Type type)
{
var xmlType = type.GetCustomAttribute<XmlRootAttribute>();
if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
return xmlType.Namespace;
return null;
}
public void Serialize(RequestRootBase root, XmlWriter writer)
{
writer.WriteStartDocument();
writer.WriteStartElement(RequestRootElementName);
writer.WriteStartElement(ActionElementName);
var typeName = BindToName(root.RequestType);
writer.WriteAttributeString(TypeAttributeName, typeName);
var serializer = new XmlSerializer(root.RequestType);
var rootNameSpace = DefaultRootXmlElementNamespace(root.RequestType);
var ns = new XmlSerializerNamespaces();
if (string.IsNullOrEmpty(rootNameSpace))
ns.Add("", "");
else
ns.Add("", rootNameSpace);
serializer.Serialize(writer, root.RequestObject, ns);
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
}
public RequestRootBase Deserialize(XmlReader reader)
{
if (!reader.ReadToFollowing(RequestRootElementName))
return null;
if (!reader.ReadToFollowing(ActionElementName))
return null;
var typeName = reader[TypeAttributeName];
if (typeName == null)
return null;
var type = BindToType(typeName);
if (type == null)
throw new InvalidDataException(); // THROW AN EXCEPTION in this case
reader.ReadStartElement();
var serializer = new XmlSerializer(type);
var action = serializer.Deserialize(reader);
if (action == null)
return null;
return RequestRootHelper.CreateBase(action);
}
public string SerializeToString(RequestRootBase root)
{
using (var textWriter = new StringWriter())
{
var settings = new XmlWriterSettings() { Indent = true, IndentChars = " " }; // For cosmetic purposes.
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
Serialize(root, xmlWriter);
return textWriter.ToString();
}
}
public RequestRootBase DeserializeFromString(string xml)
{
using (var sr = new StringReader(xml))
using (var xmlReader = XmlReader.Create(sr))
{
return Deserialize(xmlReader);
}
}
}
public class RequestRootXmlSerializer : RequestRootXmlSerializerBase
{
readonly Dictionary<string, Type> nameToType = new Dictionary<string, Type>();
readonly Dictionary<Type, string> typeToName = new Dictionary<Type, string>();
const string ListPrefix = "ArrayOf";
const string ListPostFix = "";
protected override string BindToName(Type type)
{
return typeToName[type];
}
protected override Type BindToType(string name)
{
return nameToType[name];
}
public RequestRootXmlSerializer(IEnumerable<Type> types)
{
if (types == null)
throw new ArgumentNullException();
foreach (var type in types)
{
if (type.IsInterface || type.IsAbstract)
throw new ArgumentException();
var name = DefaultXmlElementName(type);
nameToType.Add(name, type);
typeToName.Add(type, name);
}
}
static string DefaultXmlElementName(Type type)
{
if (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(List<>))
{
var elementType = type.GetGenericArguments()[0];
return ListPrefix + DefaultXmlElementName(elementType) + ListPostFix;
}
else
{
var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
return xmlRoot.ElementName;
var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
return xmlType.TypeName;
return type.Name;
}
}
}
You will probably want to replace my type-to-name mapping scheme with your own; it's just a prototype.
Then use it like:
[XmlRoot("A", Namespace="ATestNameSpace")]
public class ClassA
{
[XmlText]
public string Value { get; set; }
}
public class MyObject
{
public string ObjectValue1 { get; set; }
public string ObjectValue2 { get; set; }
}
public class TestClass
{
public static void Test()
{
var root1 = RequestRootHelper.Create(new ClassA { Value = "Some data" });
var root2 = RequestRootHelper.Create(new List<MyObject> { new MyObject { ObjectValue1 = "Object Value 1-1", ObjectValue2 = "Object Value 2-1" }, new MyObject { ObjectValue1 = "Object Value 1-2", ObjectValue2 = "Object Value 2-2" } });
var serializer = new RequestRootXmlSerializer(new[] { typeof(ClassA), typeof(List<ClassA>), typeof(MyObject), typeof(List<MyObject>) });
TestRootSerialization(root1, serializer);
TestRootSerialization(root2, serializer);
}
private static void TestRootSerialization<T>(RequestRoot<T> root, RequestRootXmlSerializer serializer)
{
var xml1 = serializer.SerializeToString(root);
Debug.WriteLine(xml1);
var root11 = serializer.DeserializeFromString(xml1);
Debug.Assert(root.GetType() == root11.GetType()); // NO ASSERT
var xml11 = serializer.SerializeToString(root11);
Debug.WriteLine(xml11);
Debug.Assert(xml1 == xml11); // NO ASSERT
}
}
This produces the following XML output for ClassA:
<RequestRoot>
<Action Type="A">
<A xmlns="ATestNameSpace">Some data</A>
</Action>
</RequestRoot>
And for List<MyObject>:
<RequestRoot>
<Action Type="ArrayOfMyObject">
<ArrayOfMyObject>
<MyObject>
<ObjectValue1>Object Value 1-1</ObjectValue1>
<ObjectValue2>Object Value 2-1</ObjectValue2>
</MyObject>
<MyObject>
<ObjectValue1>Object Value 1-2</ObjectValue1>
<ObjectValue2>Object Value 2-2</ObjectValue2>
</MyObject>
</ArrayOfMyObject>
</Action>
</RequestRoot>

Related

Wrap properties with CData Section - XML Serialization C#

I need to serialize my object in such a way that the properties I want, would get wrapped around CData sections.
I was hoping I could do something like this :
public class Order
{
[JsonProperty]
public int OrderId { get; set; }
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public int Type { get; set; }
[JsonProperty]
public decimal Amount { get; set; }
[JsonProperty]
public DateTime Date { get; set; }
[DataMember]
[JsonProperty]
**[WrapCData]**
public List<Option> ListB { get; set; }
[DataMember]
public List<string> ListC { get; set; }
**[WrapCData]**
public Product Product { get; set; }
}
Is there any attribute or an implementation which could wrap my specific properties around a CData section? Existing StackOverflow answers suggest fiddling with the Entity(Class) itself. This would get really messy.
In the following thread :
How do you serialize a string as CDATA using XmlSerializer?
Philip's answer suggests to make another property and its corresponding CData property. However the property is a string. CreateCDataSection() also takes a string. I need to wrap my custom objects/lists around CDataSections. How can I do that? Any help would be appreciated. Thanks.
Sample XML for the above Order Class:
<Order xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<OrderId>2</OrderId>
<Name>Some Name</Name>
<Type>1</Type>
<Amount>100</Amount>
<Date>2015-12-07T15:10:49.6031106+05:00</Date>
<![CDATA[
<ListB>
<Option>
<OptionValue>OptionValue1</OptionValue>
<OptionName>Option1</OptionName>
</Option>
<Option>
<OptionValue>OptionValue2</OptionValue>
<OptionName>Option2</OptionName>
</Option>
</ListB>
]]>
<ListC>
<string>ListItem1</string>
<string>ListItem2</string>
</ListC>
<![CDATA[
<Product>
<ProductId>1</ProductId>
<Name>ProductName</Name>
<Type>Product Type</Type>
</Product>
]]>
</Order>
With some effort and customization, it possible to get close to what you want, however XmlSerializer will always place the CData nodes at the end of the container element. Your example XML shows the CData nodes between specific nodes of the container element. As long as you don't need this precise control, you can use How do you serialize a string as CDATA using XmlSerializer? to do nested serializations, like so:
public class Order
{
[JsonProperty]
public int OrderId { get; set; }
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public int Type { get; set; }
[JsonProperty]
public decimal Amount { get; set; }
[JsonProperty]
public DateTime Date { get; set; }
[DataMember]
[JsonProperty]
[XmlIgnore] // Do not serialize directly
[XmlWrapCData] // Instead include in CDATA nodes
public List<Option> ListB { get; set; }
[DataMember]
public List<string> ListC { get; set; }
[XmlIgnore] // Do not serialize directly
[XmlWrapCData] // Instead include in CDATA nodes
public Product Product { get; set; }
[XmlText] // NECESSARY TO EMIT CDATA NODES
[IgnoreDataMember]
[JsonIgnore]
public XmlNode[] CDataContent
{
get
{
return XmlWrapCDataHelper.GetCDataContent(this);
}
set
{
XmlWrapCDataHelper.SetCDataContent(this, value);
}
}
}
public class Product
{
public string ProductId { get; set; }
public string Name { get; set; }
public string Type { get; set; }
}
public class Option
{
public string OptionValue { get; set; }
public string OptionName { get; set; }
}
Using the following extension methods and custom attribute:
[System.AttributeUsage(System.AttributeTargets.Property, AllowMultiple = false)]
public class XmlWrapCDataAttribute : Attribute
{
public XmlWrapCDataAttribute() { this.Namespace = string.Empty; }
public XmlWrapCDataAttribute(string name) : this() { this.Name = name; }
public string Name { get; set; }
public string Namespace { get; set; }
}
public static class XmlWrapCDataHelper
{
static Tuple<PropertyInfo, XmlWrapCDataAttribute> [] XmlWrapCDataProperties(Type type)
{
return type.GetProperties()
.Where(p => p.GetGetMethod() != null && p.GetSetMethod() != null)
.Select(p => Tuple.Create(p, p.GetCustomAttribute<XmlWrapCDataAttribute>()))
.Where(p => p.Item2 != null)
.ToArray();
}
public static XmlNode[] GetCDataContent(object obj)
{
var index = new object[0];
var properties = XmlWrapCDataProperties(obj.GetType());
return properties.Select(p => (XmlNode)p.Item1.GetValue(obj, index).GetCData(p.Item2.Name ?? p.Item1.Name, p.Item2.Namespace)).ToArray();
}
public static void SetCDataContent(object obj, XmlNode [] nodes)
{
if (nodes == null || nodes.Length < 1)
return;
var index = new object[0];
var properties = XmlWrapCDataProperties(obj.GetType()).ToDictionary(p => XName.Get(p.Item2.Name ?? p.Item1.Name, p.Item2.Namespace), p => p);
var xml = "<Root>" + String.Concat(nodes.Select(c => c.Value)) + "</Root>";
foreach (var element in XElement.Parse(xml).Elements())
{
Tuple<PropertyInfo, XmlWrapCDataAttribute> pair;
if (properties.TryGetValue(element.Name, out pair))
{
var value = element.Deserialize(pair.Item1.PropertyType, element.Name.LocalName, element.Name.Namespace.NamespaceName);
pair.Item1.SetValue(obj, value, index);
}
}
}
}
public static class XmlSerializationHelper
{
public static XmlCDataSection GetCData(this object obj, string rootName, string rootNamespace)
{
return obj == null ? null : new System.Xml.XmlDocument().CreateCDataSection(obj.GetXml(XmlSerializerFactory.Create(obj.GetType(), rootName, rootNamespace)));
}
public static XCData GetCData(this object obj, XmlSerializer serializer = null)
{
return obj == null ? null : new XCData(obj.GetXml(serializer));
}
public static string GetXml(this object obj, XmlSerializer serializer = null)
{
using (var textWriter = new StringWriter())
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
var settings = new XmlWriterSettings() { Indent = true, IndentChars = " ", OmitXmlDeclaration = true }; // For cosmetic purposes.
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
(serializer ?? new XmlSerializer(obj.GetType())).Serialize(xmlWriter, obj, ns);
return textWriter.ToString();
}
}
public static object Deserialize(this XContainer element, Type type, string rootName = null, string rootNamespace = null)
{
return element.Deserialize(type, XmlSerializerFactory.Create(type, rootName, rootNamespace));
}
public static object Deserialize(this XContainer element, Type type, XmlSerializer serializer = null)
{
using (var reader = element.CreateReader())
{
return (serializer ?? new XmlSerializer(type)).Deserialize(reader);
}
}
public static T DeserializeXML<T>(this string xmlString, XmlSerializer serializer = null)
{
using (StringReader reader = new StringReader(xmlString))
{
return (T)(serializer ?? new XmlSerializer(typeof(T))).Deserialize(reader);
}
}
}
public static class XmlSerializerFactory
{
readonly static Dictionary<Tuple<Type, string, string>, XmlSerializer> cache;
readonly static object padlock;
static XmlSerializerFactory()
{
padlock = new object();
cache = new Dictionary<Tuple<Type, string, string>, XmlSerializer>();
}
public static XmlSerializer Create(Type serializedType, string rootName, string rootNamespace)
{
if (serializedType == null)
throw new ArgumentNullException();
if (rootName == null && rootNamespace == null)
return new XmlSerializer(serializedType);
lock (padlock)
{
XmlSerializer serializer;
var key = Tuple.Create(serializedType, rootName, rootNamespace);
if (!cache.TryGetValue(key, out serializer))
cache[key] = serializer = new XmlSerializer(serializedType, new XmlRootAttribute { ElementName = rootName, Namespace = rootNamespace });
return serializer;
}
}
}
This will parse your provided XML successfully, and in return generate XML that looks like:
<Order>
<OrderId>2</OrderId>
<Name>Some Name</Name>
<Type>1</Type>
<Amount>100</Amount>
<Date>2015-12-07T05:10:49.6031106-05:00</Date>
<ListC>
<string>ListItem1</string>
<string>ListItem2</string>
</ListC><![CDATA[<ListB>
<Option>
<OptionValue>OptionValue1</OptionValue>
<OptionName>Option1</OptionName>
</Option>
<Option>
<OptionValue>OptionValue2</OptionValue>
<OptionName>Option2</OptionName>
</Option>
</ListB>]]><![CDATA[<Product>
<ProductId>1</ProductId>
<Name>ProductName</Name>
<Type>Product Type</Type>
</Product>]]></Order>

JSON deserialization of variable named parameter using DataContract

Assuming we have a JSON object similar to:
{
'12345': 'text string',
'rel': 'myResource'
}
Constructing a DataContract to map to this type seems fairly simple such as:
[DataContract]
MyResource
{
[DataMember(Name = "12345")]
public string SpecialValue { get; set; }
[DataMember(Name = "rel")]
public string Rel { get; set; }
}
Now the problem arrives that the name of the property is variable so it not guaranteed to be '12345'. Since this variable cannot be properly mapped using attributes it won't get picked up when using DataContractJsonSerializer.
If I change the class to support IExtensibleDataObject, I can get the value portion but not the property name which is a problem. I'm looking to maintain this value during deserialization/serialization in order to be able to send the information on a return request. I'm not looking to change over to using Json.NET to solve this problem as I want to know if it is possible in some form without resorting to an external dependency.
It's a little ugly, but it turns out you can use an IDataContractSurrogate to deserialize the class with the variably named property into a Dictionary<string, object> and then copy the values from the dictionary into your class. Of course, you will need to add another property to your class to hold the name of the "special" property.
Here is an example surrogate that I was able to get working:
class MyDataContractSurrogate : IDataContractSurrogate
{
public Type GetDataContractType(Type type)
{
if (type == typeof(MyResource))
{
return typeof(Dictionary<string, object>);
}
return type;
}
public object GetDeserializedObject(object obj, Type targetType)
{
if (obj.GetType() == typeof(Dictionary<string, object>) &&
targetType == typeof(MyResource))
{
Dictionary<string, object> dict = (Dictionary<string, object>)obj;
MyResource mr = new MyResource();
foreach (PropertyInfo prop in GetInterestingProperties(typeof(MyResource)))
{
DataMemberAttribute att = prop.GetCustomAttribute<DataMemberAttribute>();
object value;
if (dict.TryGetValue(att.Name, out value))
{
prop.SetValue(mr, value);
dict.Remove(att.Name);
}
}
// should only be one property left in the dictionary
if (dict.Count > 0)
{
var kvp = dict.First();
mr.SpecialName = kvp.Key;
mr.SpecialValue = (string)kvp.Value;
}
return mr;
}
return obj;
}
public object GetObjectToSerialize(object obj, Type targetType)
{
if (obj.GetType() == typeof(MyResource) &&
targetType == typeof(Dictionary<string, object>))
{
MyResource mr = (MyResource)obj;
Dictionary<string, object> dict = new Dictionary<string, object>();
dict.Add(mr.SpecialName, mr.SpecialValue);
foreach (PropertyInfo prop in GetInterestingProperties(typeof(MyResource)))
{
DataMemberAttribute att = prop.GetCustomAttribute<DataMemberAttribute>();
dict.Add(att.Name, prop.GetValue(mr));
}
return dict;
}
return obj;
}
private IEnumerable<PropertyInfo> GetInterestingProperties(Type type)
{
return type.GetProperties().Where(p => p.CanRead && p.CanWrite &&
p.GetCustomAttribute<DataMemberAttribute>() != null);
}
// ------- The rest of these methods are not needed -------
public object GetCustomDataToExport(Type clrType, Type dataContractType)
{
throw new NotImplementedException();
}
public object GetCustomDataToExport(MemberInfo memberInfo, Type dataContractType)
{
throw new NotImplementedException();
}
public void GetKnownCustomDataTypes(System.Collections.ObjectModel.Collection<Type> customDataTypes)
{
throw new NotImplementedException();
}
public Type GetReferencedTypeOnImport(string typeName, string typeNamespace, object customData)
{
throw new NotImplementedException();
}
public System.CodeDom.CodeTypeDeclaration ProcessImportedType(System.CodeDom.CodeTypeDeclaration typeDeclaration, System.CodeDom.CodeCompileUnit compileUnit)
{
throw new NotImplementedException();
}
}
To use the surrogate, you'll need to create an instance of DataContractJsonSerializerSettings and pass it to the DataContractJsonSerializer with the following properties set. Note that since we require the UseSimpleDictionaryFormat setting, this solution will only work with .Net 4.5 or later.
var settings = new DataContractJsonSerializerSettings();
settings.DataContractSurrogate = new MyDataContractSurrogate();
settings.KnownTypes = new List<Type> { typeof(Dictionary<string, object>) };
settings.UseSimpleDictionaryFormat = true;
Note also that in your class you should NOT mark the "special" properties with a [DataMember] attribute, since they are handled specially in the surrogate.
[DataContract]
class MyResource
{
// Don't mark these with [DataMember]
public string SpecialName { get; set; }
public string SpecialValue { get; set; }
[DataMember(Name = "rel")]
public string Rel { get; set; }
}
Here is a demo:
class Program
{
static void Main(string[] args)
{
string json = #"
{
""12345"": ""text string"",
""rel"": ""myResource""
}";
var settings = new DataContractJsonSerializerSettings();
settings.DataContractSurrogate = new MyDataContractSurrogate();
settings.KnownTypes = new List<Type> { typeof(Dictionary<string, object>) };
settings.UseSimpleDictionaryFormat = true;
MyResource mr = Deserialize<MyResource>(json, settings);
Console.WriteLine("Special name: " + mr.SpecialName);
Console.WriteLine("Special value: " + mr.SpecialValue);
Console.WriteLine("Rel: " + mr.Rel);
Console.WriteLine();
json = Serialize(mr, settings);
Console.WriteLine(json);
}
public static T Deserialize<T>(string json, DataContractJsonSerializerSettings settings = null)
{
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(json)))
{
if (settings == null) settings = GetDefaultSerializerSettings();
var ser = new DataContractJsonSerializer(typeof(T), settings);
return (T)ser.ReadObject(ms);
}
}
public static string Serialize(object obj, DataContractJsonSerializerSettings settings = null)
{
using (MemoryStream ms = new MemoryStream())
{
if (settings == null) settings = GetDefaultSerializerSettings();
var ser = new DataContractJsonSerializer(obj.GetType(), settings);
ser.WriteObject(ms, obj);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
}
Output:
Special name: 12345
Special value: text string
Rel: myResource
{"12345":"text string","rel":"myResource"}

How can I define my classes such that an XmlAttribute text (not its value) for a node can be specified at run-time, without custom serializers?

How can I define my class structure such that I can assign the XmlAttribute text (not it's value) for a given element during run-time? For example, I have the following XML where I want to define a unique value for each instance of a specific XML note attribute text noted below as, "DEFINE_UNIQUE_FOR_EACH_INSTANCE":
<MyXml>
<ZipCode DEFINE_UNIQUE_FOR_EACH_INSTANCE="Postal Code">90210</ZipCode>
<State DEFINE_UNIQUE_FOR_EACH_INSTANCE="US State">CA</State>
</MyXml>
So I'd like to get something like:
<MyXml>
<ZipCode labelText="Postal Code">90210</ZipCode>
<State defaultValue="US State">CA</State>
</MyXml>
Here is my class definition for defining the first XML noted above:
[XmlRootAttribute]
public class MyXml
{
public XmlValueAndAttribute ZipCode { get; set; }
public XmlValueAndAttribute State { get; set; }
public MyXml()
{
ZipCode = new XmlValueAndAttribute(); State = new XmlValueAndAttribute();
}
}
And
public class XmlValueAndAttribute
{
[XmlAttribute("DEFINE_UNIQUE_FOR_EACH_INSTANCE")]
public string AttributeValue { get; set; }
[XmlText]
public string Value { get; set; }
public XmlValueAndAttribute() { }
public XmlValueAndAttribute(string value, string attribute)
{
Value = value;
AttributeValue = attribute;
}
}
And the usage of the class. Note the commented out code noting how I would like to make the attribute text assignment:
static void Main(string[] args)
{
MyXml xml = new MyXml();
xml.ZipCode = new XmlValueAndAttribute("90210", "Postal Code" /*, "labelText"*/ )
xml.State = new XmlValueAndAttribute("CA", "US State" /*"defaultValue"*/);
XmlSerializer x = new XmlSerializer(xml.GetType());
var xmlnsEmpty = new XmlSerializerNamespaces();
xmlnsEmpty.Add("", "");
x.Serialize(Console.Out, xml, xmlnsEmpty);
Console.ReadKey();
}
Thanks.
You can do this by having your XmlValueAndAttribute class implement IXmlSerializable:
public class XmlValueAndAttribute : IXmlSerializable
{
public string AttributeName { get; set; }
public string AttributeValue { get; set; }
public string Value { get; set; }
public XmlValueAndAttribute() { }
public XmlValueAndAttribute(string value, string attribute, string attributeName)
{
Value = value;
AttributeValue = attribute;
AttributeName = attributeName;
}
#region IXmlSerializable Members
public XmlSchema GetSchema()
{
return null;
}
static XName nilName = XName.Get("nil", "http://www.w3.org/2001/XMLSchema-instance");
public void ReadXml(XmlReader reader)
{
using (var subReader = reader.ReadSubtree())
{
var element = XElement.Load(subReader);
reader.Read(); // Advance past the end of the element.
if (element == null)
return;
Value = (bool?)element.Attribute(nilName) == true ? null : element.Value;
var attr = element.Attributes().Where(a => a.Name != nilName && !a.IsNamespaceDeclaration).FirstOrDefault();
if (attr != null)
{
AttributeName = XmlConvert.DecodeName(attr.Name.LocalName);
AttributeValue = attr.Value;
}
}
}
public void WriteXml(XmlWriter writer)
{
if (!string.IsNullOrEmpty(AttributeName))
writer.WriteAttributeString(XmlConvert.EncodeLocalName(AttributeName), AttributeValue);
if (Value == null)
writer.WriteAttributeString("xsi", nilName.LocalName, nilName.Namespace.ToString(), XmlConvert.ToString(true));
else
writer.WriteString(Value);
}
#endregion
}
Note that this implementation properly captures a null value for the Value property by writing xsi:nil="true", but if AttributeValue is null while AttributeName is non-empty, the AttributeValue will get converted to an empty string when deserialized.
If you try to replace you DEFINE_UNIQUE_FOR_EACH_INSTANCE with something else, like a variable, you receive the following error:
An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type
This indicates clearly, that the value of the parameter must exist at compile time. So in my opinion there seems to be no easy way except IL waeaving or some other hack.

how to derive xml element name from an attribute value of a class using annotations?

I have
properties that have ids and values and a name. Can I represent all those with a single class using XmlElement/XmlArray C# annotations? I would like to derive the xml element name from the class attribute name;
my class would look like:
public class Property {
public string name; //could be enum
public int id;
public string value;
}
e.g:
new Property("property1name",2,"testvalue");
new Property("property2name",10,"anothervalue");
I would like to have xml that looks like:
<property1name><id>2</id><value>testvalue</value></property1name>
<property2name><id>10</id><value>anothervalue</value></property2name>
instead of the usual
<property><name>property1name</name><id>2</id><value>testvalue</value></property>
<property><name>property2name</name><id>10</id><value>anothervalue</value></property>
In other words the xmlelement gets its name from attribute name of the class Property
Update
And here's a quick adaptation to handle your Property class. First, a List<T> subclass that implements IXmlSerializable:
public interface IHasElementName
{
string ElementName { get; set; }
}
public class XmlNamedElementList<T> : List<T>, IXmlSerializable where T : IHasXmlElementName
{
// https://msdn.microsoft.com/en-us/library/System.Xml.Serialization.XmlSerializer%28v=vs.110%29.aspx
// Any serializer created with the "XmlSerializer(typeof(T), rootAttribute)" must be cached
// to avoid resource & memory leaks.
class ValueSerializerCache
{
// By using a nested class with a static constructor, we defer generation of the XmlSerializer until it's actually required.
static ValueSerializerCache()
{
var rootAttribute = new XmlRootAttribute();
rootAttribute.ElementName = ValueTypeName;
rootAttribute.Namespace = ValueTypeNamespace;
serializer = new XmlSerializer(typeof(T), rootAttribute);
}
static readonly XmlSerializer serializer;
internal static XmlSerializer Serializer { get { return serializer; } }
}
static string ValueTypeName { get { return typeof(T).DefaultXmlElementName(); } }
static string ValueTypeNamespace { get { return typeof(T).DefaultXmlElementNamespace(); } }
#region IXmlSerializable Members
XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
void IXmlSerializable.ReadXml(XmlReader reader)
{
if (reader.IsEmptyElement)
{
reader.Read();
return;
}
var typeName = ValueTypeName;
reader.ReadStartElement(); // Advance to the first sub element of the list element.
while (reader.NodeType == XmlNodeType.Element)
{
var name = reader.Name;
using (var subReader = reader.ReadSubtree())
{
var doc = XDocument.Load(subReader);
if (doc != null && doc.Root != null)
{
doc.Root.Name = doc.Root.Name.Namespace + typeName;
using (var docReader = doc.CreateReader())
{
var obj = ValueSerializerCache.Serializer.Deserialize(docReader);
if (obj != null)
{
T value = (T)obj;
value.ElementName = XmlConvert.DecodeName(name);
Add(value);
}
}
}
}
// Move past the end of item element
reader.Read();
}
// Move past the end of the list element
reader.ReadEndElement();
}
void IXmlSerializable.WriteXml(XmlWriter writer)
{
foreach (var value in this)
{
XDocument doc = new XDocument();
using (var subWriter = doc.CreateWriter())
{
// write xml into the writer
ValueSerializerCache.Serializer.Serialize(subWriter, value);
}
if (doc.Root == null)
continue;
doc.Root.Name = doc.Root.Name.Namespace + XmlConvert.EncodeName(value.ElementName);
// Remove redundant namespaces.
foreach (var attr in doc.Root.Attributes().ToList())
{
if (!attr.IsNamespaceDeclaration || string.IsNullOrEmpty(attr.Value))
continue;
var prefix = writer.LookupPrefix(attr.Value);
if ((prefix == attr.Name.LocalName)
|| (prefix == string.Empty && attr.Name == "xmlns"))
attr.Remove();
}
doc.Root.WriteTo(writer);
}
}
#endregion
}
public static class XmlSerializationHelper
{
static Attribute GetCustomAttribute(MemberInfo element, Type attributeType)
{
return Attribute.GetCustomAttribute(element, attributeType);
}
static T GetCustomAttribute<T>(MemberInfo element) where T : Attribute
{
return (T)GetCustomAttribute(element, typeof(T));
}
public static string DefaultXmlElementName(this Type type)
{
var xmlType = GetCustomAttribute<XmlTypeAttribute>(type);
if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
return xmlType.TypeName;
var xmlRoot = GetCustomAttribute<XmlRootAttribute>(type);
if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
return xmlRoot.ElementName;
return type.Name;
}
public static string DefaultXmlElementNamespace(this Type type)
{
var xmlType = GetCustomAttribute<XmlTypeAttribute>(type);
if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
return xmlType.Namespace;
var xmlRoot = GetCustomAttribute<XmlRootAttribute>(type);
if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.Namespace))
return xmlRoot.Namespace;
return string.Empty;
}
public static string GetXml<T>(this T obj)
{
return GetXml(obj, false);
}
public static string GetXml<T>(this T obj, bool omitNamespace)
{
return GetXml(obj, new XmlSerializer(obj.GetType()), omitNamespace);
}
public static string GetXml<T>(this T obj, XmlSerializer serializer)
{
return GetXml(obj, serializer, false);
}
public static string GetXml<T>(T obj, XmlSerializer serializer, bool omitStandardNamespaces)
{
XmlSerializerNamespaces ns = null;
if (omitStandardNamespaces)
{
ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
}
return GetXml(obj, serializer, ns);
}
public static string GetXml<T>(T obj, XmlSerializerNamespaces ns)
{
return GetXml(obj, new XmlSerializer(obj.GetType()), ns);
}
public static string GetXml<T>(T obj, XmlSerializer serializer, XmlSerializerNamespaces ns)
{
using (var textWriter = new StringWriter())
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true; // For cosmetic purposes.
settings.IndentChars = " "; // For cosmetic purposes.
using (var xmlWriter = XmlWriter.Create(textWriter, settings))
{
if (ns != null)
serializer.Serialize(xmlWriter, obj, ns);
else
serializer.Serialize(xmlWriter, obj);
}
return textWriter.ToString();
}
}
}
And use it like:
public class Property : IHasElementName
{
public Property()
{
}
public Property(string name, int id, string value)
{
this.name = name;
this.id = id;
this.value = value;
}
[XmlIgnore]
public string name; //could be enum
public int id;
public string value;
#region IHasElementName Members
[XmlIgnore]
string IHasElementName.ElementName { get { return name; } set { name = value; } }
#endregion
}
public class RootObject
{
public RootObject()
{
this.Properties = new XmlNamedElementList<Property>();
}
public XmlNamedElementList<Property> Properties { get; set; }
}
public static class TestClass
{
public static void Test()
{
var root = new RootObject
{
// Characters " <> first" in the first element name are for testing purposes.
Properties = new XmlNamedElementList<Property> { new Property { id = 1, value = "1", name = "first" }, new Property("property1name", 2, "testvalue"), new Property("property2name", 10, "anothervalue") }
};
var xml = root.GetXml();
Debug.WriteLine(xml);
}
}
Which produces XML as follows:
<?xml version="1.0" encoding="utf-16"?>
<RootObject xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Properties>
<_x0020__x003C__x003E__x0020_first>
<id>1</id>
<value>1</value>
</_x0020__x003C__x003E__x0020_first>
<property1name>
<id>2</id>
<value>testvalue</value>
</property1name>
<property2name>
<id>10</id>
<value>anothervalue</value>
</property2name>
</Properties>
</RootObject>
Original Answer
As requested, here's an implementation of IXmlSerializable on a List<KeyValuePair<string, T>> in which the Key string becomes the element name in the collection.
What you would probably want to do is to adapt this to serialize a List<IHasElementName> where:
public interface IHasElementName
{
string ElementName { get; set; }
}
public class Property : IHasElementName
{
[XmlIgnore]
public string name; //could be enum
public int id;
public string value;
#region IHasElementName Members
[XmlIgnore]
string IHasElementName.ElementName
{
get
{
return name;
}
set
{
name = value;
}
}
#endregion
}
If the name is actually an Enum, you could return the enum string representation from HasElementName.ElementName.
The list looks like:
public class XmlKeyValueList<T> : List<KeyValuePair<string, T>>, IXmlSerializable
{
// TODO: validate that the "Key" string using XmlConvert.VerifyName.
// https://msdn.microsoft.com/en-us/library/System.Xml.Serialization.XmlSerializer%28v=vs.110%29.aspx
// Any serializer created with the "XmlSerializer(typeof(T), rootAttribute)" must be cached
// to avoid resource & memory leaks.
class ValueSerializerCache
{
// By using a nested class with a static constructor, we defer generation of the XmlSerializer until it's actually required.
static ValueSerializerCache()
{
var rootAttribute = new XmlRootAttribute();
rootAttribute.ElementName = ValueTypeName;
rootAttribute.Namespace = ValueTypeNamespace;
serializer = new XmlSerializer(typeof(T), rootAttribute);
}
static readonly XmlSerializer serializer;
internal static XmlSerializer Serializer { get { return serializer; } }
}
static string ValueTypeName { get { return typeof(T).DefaultXmlElementName(); } }
static string ValueTypeNamespace { get { return typeof(T).DefaultXmlElementNamespace(); } }
#region IXmlSerializable Members
XmlSchema IXmlSerializable.GetSchema()
{
return null;
}
void IXmlSerializable.ReadXml(XmlReader reader)
{
var typeName = ValueTypeName;
reader.ReadStartElement(); // Advance to the first sub element of the list element.
while (reader.NodeType == XmlNodeType.Element)
{
var name = reader.Name;
using (var subReader = reader.ReadSubtree())
{
var doc = XDocument.Load(subReader);
if (doc != null && doc.Root != null)
{
doc.Root.Name = typeName;
using (var docReader = doc.CreateReader())
{
var obj = ValueSerializerCache.Serializer.Deserialize(docReader);
if (obj != null)
{
Add(new KeyValuePair<string, T>(name, (T)obj));
}
}
}
}
// Move past the XmlNodeType.Element
if (reader.NodeType == XmlNodeType.EndElement)
reader.Read();
}
}
void IXmlSerializable.WriteXml(XmlWriter writer)
{
foreach (var pair in this)
{
XDocument doc = new XDocument();
using (var subWriter = doc.CreateWriter())
{
// write xml into the writer
ValueSerializerCache.Serializer.Serialize(subWriter, pair.Value);
}
if (doc.Root == null)
continue;
doc.Root.Name = pair.Key;
// Remove redundant namespaces.
foreach (var attr in doc.Root.Attributes().ToList())
{
if (!attr.IsNamespaceDeclaration || string.IsNullOrEmpty(attr.Value))
continue;
if (writer.LookupPrefix(attr.Value) == attr.Name.LocalName)
attr.Remove();
}
doc.Root.WriteTo(writer);
}
}
#endregion
}
public static class XmlSerializationHelper
{
public static string DefaultXmlElementName(this Type type)
{
var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
if (xmlType != null && !string.IsNullOrEmpty(xmlType.TypeName))
return xmlType.TypeName;
var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.ElementName))
return xmlRoot.ElementName;
return type.Name;
}
public static string DefaultXmlElementNamespace(this Type type)
{
var xmlType = type.GetCustomAttribute<XmlTypeAttribute>();
if (xmlType != null && !string.IsNullOrEmpty(xmlType.Namespace))
return xmlType.Namespace;
var xmlRoot = type.GetCustomAttribute<XmlRootAttribute>();
if (xmlRoot != null && !string.IsNullOrEmpty(xmlRoot.Namespace))
return xmlRoot.Namespace;
return string.Empty;
}
}
If UnitItem is changed into
public class UnitItem
{
public string AAA;
public string BBB;
}
then the XML will be
<Items>
<UnitItem>
<AAA>testa</AAA>
<BBB>testb</BBB>
</UnitItem>
</Items>

XmlSerializer putting element name inside a property when deserializing

I have an issue with the .NET XML Serializer where I have do different XML Elements with different names that map to the same type. Basically, the objects should be exactly the same, but I want them to have a string or enum or something that identifies which of the three possible element names were used. So here's an example:
<Body>
<MyTypeA>
<Foo>bar</Foo>
</MyTypeA>
<MyTypeB>
<Foo>bar</Foo>
</MyTypeB>
</Body>
Now, for the classes, MyTypeA and MyTypeB will both be the same type. For example:
public class Body {
public MyType MyTypeA { get; set; }
public MyType MyTypeB { get; set; }
}
public class MyType {
public string Foo { get; set; }
[XmlIgnore]
public MyTypeType { get; set; }
}
public enum MyTypeType
{
MyTypeA,
MyTypeB
}
When serializing it works fine, because I can always just ensure one way or another that the enum is set properly before serializing. But when deserializing it is not getting set and I'm not sure there's a way how.
For the record, I unfortunately don't get to set the schema, otherwise I would have built it in such a way that I didn't have this problem.
If i understood your question correctly, this might help you. Just write you XML file path on the 4th line and try it.
namespace ConsoleApplication1
{
class Program
{
//private const string xmlPath = #"C:\Users\Jumast\Desktop\StackOverflowQuestion.xml";
private const string xmlPath, // put the file path here
static Body makeBody()
{
var instance1 = new MyType() { Category = Category.MyTypeA, Foo = "bar" };
var instance2 = new MyType() { Category = Category.MyTypeB, Foo = "bar" };
return new Body(){Instance1 = instance1, Instance2 = instance2};
}
static void serializeBody(Body body, string path)
{
var ser = new DataContractSerializer(body.GetType(), body.GetType().Name, "");
using (var w = XmlWriter.Create(path, new XmlWriterSettings() { Indent = true }))
{
ser.WriteObject(w, body);
}
}
static Body deseerializeBody(string xmlPath)
{
Body deserializedBody;
var ser = new XmlSerializer(typeof(Body));
using (Stream stream = File.OpenRead(xmlPath))
{
deserializedBody = (Body)ser.Deserialize(stream);
}
return deserializedBody;
}
static void writeBodyToConsole(Body body)
{
Console.WriteLine("Instance1: " + body.Instance1);
Console.WriteLine("Instance2: " + body.Instance2);
Console.ReadKey();
}
static void Main(string[] args)
{
serializeBody(makeBody(), xmlPath);
writeBodyToConsole(deseerializeBody(xmlPath));
}
}
public class Body : IXmlSerializable
{
#region Properties
public MyType Instance1 { get; set; }
public MyType Instance2 { get; set; }
#endregion
#region IXmlSerializable
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
reader.ReadStartElement();
Instance1 = new MyType(reader);
Instance2 = new MyType(reader);
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
Instance1.WriteXml(writer);
Instance2.WriteXml(writer);
}
#endregion
}
public class MyType : IXmlSerializable
{
#region Fields
private Category _category;
#endregion
#region Constructors
public MyType()
{
_category = Category.nil;
Foo = string.Empty;
}
public MyType(XmlReader reader) { ReadXml(reader);}
#endregion
#region Methods
public override string ToString()
{
var sb = new StringBuilder();
sb.Append(string.Format("Foo = {0}", Foo));
sb.Append(" , ");
sb.Append(string.Format("Category = {0}", Category));
return sb.ToString();
}
#endregion
#region Properties
public string Foo { get; set; }
public Category Category
{
get { return this._category; }
set { this._category = value; }
}
#endregion
#region IXmlSerializable
public System.Xml.Schema.XmlSchema GetSchema()
{
return null;
}
public void ReadXml(System.Xml.XmlReader reader)
{
Enum.TryParse(reader.Name, out _category);
reader.Read();
Foo = reader.ReadElementContentAsString("Foo", "");
reader.ReadEndElement();
}
public void WriteXml(System.Xml.XmlWriter writer)
{
writer.WriteStartElement(this.Category.ToString(), "");
writer.WriteElementString("Foo", Foo);
writer.WriteEndElement();
}
#endregion
}
public enum Category
{
MyTypeA,
MyTypeB,
nil
}
}

Categories