I have an issue when deserializing an object (which I cannot modify).
I receive an exponential value for certain xml element and for represent them in my class I used decimal value expect that when I deserialize the xml document it fails.
<fwdRate>-9.72316862724032E-05</fwdRate>
Is there any solution to represent this attribute other than create 2 attributes in my class to represent it (one string and the other a decimal value)?
Can I create a custom deserializtion class for decimal value?
private void ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(SwapDataSynapseResult));
using (TextReader reader = new StringReader(value))
{
_result = serializer.Deserialize(reader) as SwapDataSynapseResult;
}
}
As Demand
using System;
using System.IO;
using System.Xml.Serialization;
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public decimal FwdRate { get; set; }
}
public class Program
{
public static void Main()
{
string val = "<result><fwdRate>-9.72316862724032E-05</fwdRate></result>";
Result response = ParseXML(val);
}
static Result ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(Result));
using (TextReader reader = new StringReader(value))
{
return serializer.Deserialize(reader) as Result;
}
}
}
In XML, decimal values are not allowed to use scientific (exponential) notation (See this link at the 'Restrictions' paragraph).
Either:
the value is indeed a floating point one: Put a float/double instead of a decimal in the code.
the XML is corrupted.
In the same way, in C#, by default, Decimal.Parse doesn't accept exponential representation.
You can override this behavior by implementing a new struct that wrap a decimal and implement IXmlSerializable and allow exponential representation when de-serialized:
public struct XmlDecimal : IXmlSerializable
{
public decimal Value { get; private set; }
public XmlDecimal(decimal value) => Value = value;
public XmlSchema GetSchema() => null;
public void ReadXml(XmlReader reader)
{
var s = reader.ReadElementContentAsString();
Value = decimal.TryParse(s, NumberStyles.Number | NumberStyles.AllowExponent,
NumberFormatInfo.InvariantInfo, out var value)
? value
: 0; // If parse fail the resulting value is 0. Maybe we can throw an exception here.
}
public void WriteXml(XmlWriter writer) => writer.WriteValue(Value);
public static implicit operator decimal(XmlDecimal v) => v.Value;
public override string ToString() => Value.ToString();
}
The flaw is that you have to use this struct instead of a decimal everywhere in your model.
And sadly you can't make this struct read-only, has explained here.
The proper way is to control how your properties are deserialized by implementing the IXmlSerializable interface:
IXmlSerializable
IXmlSerializable code project example
In the ReadXml method you should convert your number
var yourStringNumber = ...
this.fwdRate = Decimal.Parse(yourStringNumber, System.Globalization.NumberStyles.Float);
But this method will require you to parse of the whole xml manually that is a bit overhead sometimes.
A simple solution (that smells but might be useful) is just to add additional fwdRateDecimal field to your class and fulfill the value after the serialization.
private void ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(SwapDataSynapseResult));
using (TextReader reader = new StringReader(value))
{
_result = serializer.Deserialize(reader) as SwapDataSynapseResult;
_result.fwdRateDecimal = Decimal.Parse(_result.fwdRate, System.Globalization.NumberStyles.Float)
}
}
Also conversion can be implemented in a type directly:
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public string FwdRateStr { get; set; }
private string lastParsedValue = null;
private decimal fwdRate = 0;
[XmlIgnore]
public decimal FwdRate
{
get
{
if(FwdRateStr != lastParsedValue)
{
lastParsedValue = FwdRateStr
fwdRate = Decimal.Parse(FwdRateStr ,System.Globalization.NumberStyles.Float)
}
return fwdRate
}
}
This is the solution:
using System;
using System.IO;
using System.Xml.Serialization;
[XmlRoot(ElementName = "result")]
public class Result
{
[XmlElement(ElementName = "fwdRate")]
public double FwdRate { get; set; }
}
public class Program
{
public static void Main()
{
string val = "<result><fwdRate>-9.72316862724032E-05</fwdRate></result>";
Result response = ParseXML(val);
Console.WriteLine(response.FwdRate);
}
static Result ParseXML(string value)
{
XmlSerializer serializer = new XmlSerializer(typeof(Result));
using (TextReader reader = new StringReader(value))
{
return serializer.Deserialize(reader) as Result;
}
}
}
You can't modify the incoming xml but you can modify how you read its' data.
Read that number as a double instead of decimal and you will have the right decimal precision.
Output:
-9,72316862724032E-05
Related
I'm trying to deserialize some xml into a C# object. The trick is for the most part, I know what this object will look like. However, this is one child that has dynamic elements.
(here is an example)
<measurement>
<Time>2021-02-02</Time>
<ID>1</ID>
<LeftWheel>
<ValuesRead>
<DynamicValue>12.3</DynamicValue>
<DynamicValue2>2.3</DynamicValue2>
<DynamicValue4>1.3</DynamicValue4>
<DynamicValue3>10.3</DynamicValue3>
</ValuesRead>
</LeftWheel>
<RightWheel>
<ValuesRead>
<DynamicValue>12.3</DynamicValue>
<DynamicValue2>2.3</DynamicValue2>
<DynamicValue6>1.3</DynamicValue6>
<DynamicValue10>10.3</DynamicValue10>
</ValuesRead>
</RightWheel>
</measurement>
In this XML, Measurement, Time, and ID are always going to in the object.
The LeftWheel and RightWheel elements are always going to be there with ValuesRead, but the ValuesRead children are dynamic and can be anything.
I have tried making a C# object to reflect most the structure, and then using the XmlSerializer.UnknownElement to pick up the unknown elements in the ValuesRead element, but I cannot link it to the parent above to know if it is on the LeftWheel or RightWheel.
XmlSerializer serializer = new XmlSerializer(typeof(FVISSiteEvent));
serializer.UnknownElement += UnknownElementFound;
Is there a way I can define the LeftWheel and RightWheel classes to be dynamic for the serialization, while having the other classes not dynamic?
You should be able to use the UnknownElementFound event to manually handle these aspects of serialization. See: Serialize XML array of unknown element name
Other options could be to specify the types you expect to see as XmlElementAtrribute decorated properties and they will just be null if they aren’t deserialized.
There’s also the nuclear option of implementing IXmlSerializable in your class and taking full control of the deserialization.
Uses Custom Serializer :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.Linq;
using System.Xml.Schema;
namespace ConsoleApplication16
{
class Program
{
const string INPUT_FILENAME = #"c:\temp\test.xml";
const string OUTPUT_FILENAME = #"c:\temp\test1.xml";
static void Main(string[] args)
{
XmlReader reader = XmlReader.Create(INPUT_FILENAME);
XmlSerializer serializer = new XmlSerializer(typeof(Measurement));
Measurement measurement = (Measurement)serializer.Deserialize(reader);
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
XmlWriter writer = XmlWriter.Create(OUTPUT_FILENAME,settings);
serializer.Serialize(writer, measurement);
}
}
[XmlRoot("measurement")]
public class Measurement
{
public DateTime Time { get; set; }
public int ID { get; set; }
[XmlArray("LeftWheel")]
[XmlArrayItem("ValuesRead")]
public List<Wheel> leftWheel { get; set; }
[XmlArray("RightWheel")]
[XmlArrayItem("ValuesRead")]
public List<Wheel> rightWheel { get; set; }
}
public class Wheel : IXmlSerializable
{
List<decimal> values { get; set; }
// Xml Serialization Infrastructure
public void WriteXml(XmlWriter writer)
{
int count = 0;
XElement valuesRead = new XElement("ValuesRead");
for (int i = 0; i < values.Count; i++ )
{
valuesRead.Add(new XElement("ValuesRead" + (i == 0? "" : i.ToString()), values[i]));
}
writer.WriteRaw(valuesRead.ToString());
}
public void ReadXml(XmlReader reader)
{
XElement values = (XElement)XElement.ReadFrom(reader);
this.values = values.Elements().Where(x => x.Name.LocalName.StartsWith("DynamicValue")).Select(x => (decimal)x).ToList();
}
public XmlSchema GetSchema()
{
return (null);
}
}
}
Besides using custom Xml serialization to deserialize your xml file, here is one another approach using Cinchoo ETL - an open source library to handle it simple way (those open to try it!)
Define POCO Class
public class Measurement
{
public DateTime Time { get; set; }
public int ID { get; set; }
[ChoXPath("LeftWheel/ValuesRead/*")]
public double[] LeftWheel { get; set; }
[ChoXPath("RightWheel/ValuesRead")]
public dynamic RightWheel { get; set; }
}
Deserialize using ChoETL
using (var r = ChoXmlReader<Measurement>.LoadText(xml)
.WithXPath("/")
)
{
foreach (var rec in r)
rec.Print();
}
Sample fiddle: https://dotnetfiddle.net/KtNvra
Disclaimer: I'm author of this library.
I've managed to resolve this using a dynamic type when deserializing.
When I deserialize ValuesRead, it is a defined as a dynamic type.
When deserialized, it turns into an XmlNode and from there I iterate over the node use the Name and InnerText values to read all the data.
When I deserialize a time string, using XmlSerializer.Deserialize, I expect it to take my local timezone into account so that a time string in the format
00:00:00.0000000+01:00
was parsed as 00:00, because I am in the timezone GMT+1.
Did I get that wrong?
Here is the code I am running to test xml deserialization:
using System;
using System.IO;
using System.Xml.Serialization;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Testing
{
[TestClass]
public class FooTest
{
[TestMethod]
public void Test()
{
var serializer = new XmlSerializer(typeof(Foo),
new XmlRootAttribute("Foo"));
var xml = "<Foo><TheTime>00:00:00.0000000+01:00</TheTime></Foo>";
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(xml);
writer.Flush();
stream.Position = 0;
var f = (Foo) serializer.Deserialize(stream);
Assert.AreEqual("00:00", f.TheTime.ToShortTimeString()); // actual: 01:00
}
[Serializable]
public class Foo
{
[XmlElement(DataType = "time")]
public DateTime TheTime { get; set; }
}
}
}
Unfortunately, there is no built-in type that you can deserialize a xs:time value into when it includes an offset (which is optional in the XSD spec).
Instead, you'll need to define a custom type and implement the appropriate interfaces for custom serialization and deserialization. Below is a minimal TimeOffset struct that will do just that.
[XmlSchemaProvider("GetSchema")]
public struct TimeOffset : IXmlSerializable
{
public DateTime Time { get; set; }
public TimeSpan Offset { get; set; }
public static XmlQualifiedName GetSchema(object xs)
{
return new XmlQualifiedName("time", "http://www.w3.org/2001/XMLSchema");
}
XmlSchema IXmlSerializable.GetSchema()
{
// this method isn't actually used, but is required to be implemented
return null;
}
void IXmlSerializable.ReadXml(XmlReader reader)
{
var s = reader.NodeType == XmlNodeType.Element
? reader.ReadElementContentAsString()
: reader.ReadContentAsString();
if (!DateTimeOffset.TryParseExact(s, "HH:mm:ss.FFFFFFFzzz",
CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto))
{
throw new FormatException("Invalid time format.");
}
this.Time = dto.DateTime;
this.Offset = dto.Offset;
}
void IXmlSerializable.WriteXml(XmlWriter writer)
{
var dto = new DateTimeOffset(this.Time, this.Offset);
writer.WriteString(dto.ToString("HH:mm:ss.FFFFFFFzzz", CultureInfo.InvariantCulture));
}
public string ToShortTimeString()
{
return this.Time.ToString("HH:mm", CultureInfo.InvariantCulture);
}
}
With this defined, you can now change the type of Foo.TheTime in your code to be a TimeOffset and your test will pass. You can also remove the DataType="time" in the attribute, as it's declared in the object itself via the GetSchema method.
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
}
}
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 custom Fraction class, which I'm using throughout my whole project. It's simple, it consists of a single constructor, accepts two ints and stores them. I'd like to use the DataContractSerializer to serialize my objects used in my project, some of which include Fractions as fields. Ideally, I'd like to be able to serialize such objects like this:
<Object>
...
<Frac>1/2</Frac> // "1/2" would get converted back into a Fraction on deserialization.
...
</Object>
As opposed to this:
<Object>
...
<Frac>
<Numerator>1</Numerator>
<Denominator>2</Denominator>
</Frac>
...
</Object>
Is there any way to do this using DataContracts?
I'd like to do this because I plan on making the XML files user-editable (I'm using them as input for a music game, and they act as notecharts, essentially), and want to keep the notation as terse as possible for the end user, so they won't need to deal with as many walls of text.
EDIT: I should also note that I currently have my Fraction class as immutable (all fields are readonly), so being able to change the state of an existing Fraction wouldn't be possible. Returning a new Fraction object would be OK, though.
If you add a property that represents the Frac element and apply the DataMember attribute to it rather than the other properties you will get what you want I believe:
[DataContract]
public class MyObject {
Int32 _Numerator;
Int32 _Denominator;
public MyObject(Int32 numerator, Int32 denominator) {
_Numerator = numerator;
_Denominator = denominator;
}
public Int32 Numerator {
get { return _Numerator; }
set { _Numerator = value; }
}
public Int32 Denominator {
get { return _Denominator; }
set { _Denominator = value; }
}
[DataMember(Name="Frac")]
public String Fraction {
get { return _Numerator + "/" + _Denominator; }
set {
String[] parts = value.Split(new char[] { '/' });
_Numerator = Int32.Parse(parts[0]);
_Denominator = Int32.Parse(parts[1]);
}
}
}
DataContractSerializer will use a custom IXmlSerializable if it is provided in place of a DataContractAttribute. This will allow you to customize the XML formatting in anyway you need... but you will have to hand code the serialization and deserialization process for your class.
public class Fraction: IXmlSerializable
{
private Fraction()
{
}
public Fraction(int numerator, int denominator)
{
this.Numerator = numerator;
this.Denominator = denominator;
}
public int Numerator { get; private set; }
public int Denominator { get; private set; }
public XmlSchema GetSchema()
{
throw new NotImplementedException();
}
public void ReadXml(XmlReader reader)
{
var content = reader.ReadInnerXml();
var parts = content.Split('/');
Numerator = int.Parse(parts[0]);
Denominator = int.Parse(parts[1]);
}
public void WriteXml(XmlWriter writer)
{
writer.WriteRaw(this.ToString());
}
public override string ToString()
{
return string.Format("{0}/{1}", Numerator, Denominator);
}
}
[DataContract(Name = "Object", Namespace="")]
public class MyObject
{
[DataMember]
public Fraction Frac { get; set; }
}
class Program
{
static void Main(string[] args)
{
var myobject = new MyObject
{
Frac = new Fraction(1, 2)
};
var dcs = new DataContractSerializer(typeof(MyObject));
string xml = null;
using (var ms = new MemoryStream())
{
dcs.WriteObject(ms, myobject);
xml = Encoding.UTF8.GetString(ms.ToArray());
Console.WriteLine(xml);
// <Object><Frac>1/2</Frac></Object>
}
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(xml)))
{
ms.Position = 0;
var obj = dcs.ReadObject(ms) as MyObject;
Console.WriteLine(obj.Frac);
// 1/2
}
}
}
This MSDN article describes IDataContractSurrogate Interface which:
Provides the methods needed to substitute one type for another by the
DataContractSerializer during serialization, deserialization, and
export and import of XML schema documents.
Although way too late, still may help someone. Actually, allows to change XML for ANY class.
You can do this with the DataContractSerializer, albeit in a way that feels hacky to me. You can take advantage of the fact that data members can be private variables, and use a private string as your serialized member. The data contract serializer will also execute methods at certain points in the process that are marked with [On(De)Serializ(ed|ing)] attributes - inside of those, you can control how the int fields are mapped to the string, and vice-versa. The downside is that you lose the automatic serialization magic of the DataContractSerializer on your class, and now have more logic to maintain.
Anyways, here's what I would do:
[DataContract]
public class Fraction
{
[DataMember(Name = "Frac")]
private string serialized;
public int Numerator { get; private set; }
public int Denominator { get; private set; }
[OnSerializing]
public void OnSerializing(StreamingContext context)
{
// This gets called just before the DataContractSerializer begins.
serialized = Numerator.ToString() + "/" + Denominator.ToString();
}
[OnDeserialized]
public void OnDeserialized(StreamingContext context)
{
// This gets called after the DataContractSerializer finishes its work
var nums = serialized.Split("/");
Numerator = int.Parse(nums[0]);
Denominator = int.Parse(nums[1]);
}
}
You'll have to switch back to the XMLSerializer to do that. The DataContractSerializer is a bit more restrictive in terms of being able to customise the output.