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
}
}
Is there a way to make a function return the type of object I pass in? I would like to call the one method below to return the type I pass in. Is that possible? Should I even be trying to do this? Is there a better way...short of having two different methods?
Currently, I tried the first two calls and I get back (with the first call) what looks like a dictionary with a system.object[] in the value part of the dictionary. Screen shot below might show it better than my explanation. I ask this as I might have more types that I need to deserialize and don't want to have a different method for each.
var firstTry = this.Deserialize(path, typeof(ObservableCollection<ListItemPair>();
var secondTry = this.Deserialize(path, typeof(ListItemPair));
var thirdTry = this.Deserialize(path, typeof(SomeOtherObject));
public static object Deserialize(string jsonFile, object type)
{
var myObject = new object();
try
{
using (StreamReader r = new StreamReader(jsonFile))
{
var serializer = new JavaScriptSerializer();
string json = r.ReadToEnd();
myObject = serializer.Deserialize<object>(json);
}
}
catch (Exception ex)
{
}
return myObject ;
}
public class ListItemPair
{
public string Name
{
get;
set;
}
public object Value
{
get;
set;
}
}
object created:
Yes, you can create a generic method. Your Deserialize() method would look something like this:
public static T Deserialize<T>(string jsonFile)
{
T myObject = default(T);
try
{
using (var r = new StreamReader(jsonFile))
{
var serializer = new JavaScriptSerializer();
string json = r.ReadToEnd();
myObject = serializer.Deserialize<T>(json);
}
}
catch (Exception ex)
{
}
return myObject;
}
In this example T is a type parameter. When invoking this method, you can pass the type like this:
var firstTry = Deserialize<ObservableCollection<ListItemPair>>(path);
var secondTry = Deserialize<ListItemPair>(path);
var thirdTry = Deserialize<SomeOtherObject>(path);
One side note: I wouldn't recommend silently swallowing an exception. In this case, it is expected that the deserialization can fail. Therefore, I would change it to a TryDeserialize() method:
public static bool TryDeserialize<T>(string jsonFile, out T myObject)
{
try
{
using (var r = new StreamReader(jsonFile))
{
var serializer = new JavaScriptSerializer();
string json = r.ReadToEnd();
myObject = serializer.Deserialize<T>(json);
}
}
catch (Exception ex)
{
myObject = default(T);
return false;
}
return true;
}
I know that the same problem is faced by a lot of people in one way or another but what I'm confused about is that how come Newtonsoft JSON Serializer is able to correctly handle this case while JavaScriptSerializer fails to do so.
I'm going to use the same code sample used in one of the other stackoverflow thread (JavascriptSerializer serializing property twice when "new" used in subclass)
void Main()
{
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
var json = serializer.Serialize(new Limited());
Limited status = serializer.Deserialize<Limited>(json); --> throws AmbiguousMatchException
}
public class Full
{
public String Stuff { get { return "Common things"; } }
public FullStatus Status { get; set; }
public Full(bool includestatus)
{
if(includestatus)
Status = new FullStatus();
}
}
public class Limited : Full
{
public new LimitedStatus Status { get; set; }
public Limited() : base(false)
{
Status = new LimitedStatus();
}
}
public class FullStatus
{
public String Text { get { return "Loads and loads and loads of things"; } }
}
public class LimitedStatus
{
public String Text { get { return "A few things"; } }
}
But if I use Newtonsoft Json Serializer, everythings works fine. Why? And is it possible to achieve the same using JavaScriptSerializer?
void Main()
{
var json = JsonConvert.SerializeObject(new Limited());
Limited status = JsonConvert.DeserializeObject<Limited>(json); ----> Works fine.
}
The reason this works in Json.NET is that it has specific code to handle this situation. From JsonPropertyCollection.cs:
/// <summary>
/// Adds a <see cref="JsonProperty"/> object.
/// </summary>
/// <param name="property">The property to add to the collection.</param>
public void AddProperty(JsonProperty property)
{
if (Contains(property.PropertyName))
{
// don't overwrite existing property with ignored property
if (property.Ignored)
return;
JsonProperty existingProperty = this[property.PropertyName];
bool duplicateProperty = true;
if (existingProperty.Ignored)
{
// remove ignored property so it can be replaced in collection
Remove(existingProperty);
duplicateProperty = false;
}
else
{
if (property.DeclaringType != null && existingProperty.DeclaringType != null)
{
if (property.DeclaringType.IsSubclassOf(existingProperty.DeclaringType))
{
// current property is on a derived class and hides the existing
Remove(existingProperty);
duplicateProperty = false;
}
if (existingProperty.DeclaringType.IsSubclassOf(property.DeclaringType))
{
// current property is hidden by the existing so don't add it
return;
}
}
}
if (duplicateProperty)
throw new JsonSerializationException("A member with the name '{0}' already exists on '{1}'. Use the JsonPropertyAttribute to specify another name.".FormatWith(CultureInfo.InvariantCulture, property.PropertyName, _type));
}
Add(property);
}
As you can see above, there is specific code here to prefer derived class properties over base class properties of the same name and visibility.
JavaScriptSerializer has no such logic. It simply calls Type.GetProperty(string, flags)
PropertyInfo propInfo = serverType.GetProperty(memberName,
BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public);
This method is documented to throw an exception in exactly this situation:
Situations in which AmbiguousMatchException occurs include the following:
A type contains two indexed properties that have the same name but different numbers of parameters. To resolve the ambiguity, use an overload of the GetProperty method that specifies parameter types.
A derived type declares a property that hides an inherited property with the same name, using the new modifier (Shadows in Visual Basic). To resolve the ambiguity, include BindingFlags.DeclaredOnly to restrict the search to members that are not inherited.
I don't know why Microsoft didn't add logic for this to JavaScriptSerializer. It's really a very simple piece of code; perhaps it got eclipsed by DataContractJsonSerializer?
You do have a workaround, which is to write a custom JavaScriptConverter:
public class LimitedConverter : JavaScriptConverter
{
const string StuffName = "Stuff";
const string StatusName = "Status";
public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
{
var limited = new Limited();
object value;
if (dictionary.TryGetValue(StuffName, out value))
{
// limited.Stuff = serializer.ConvertToType<string>(value); // Actually it's get only.
}
if (dictionary.TryGetValue(StatusName, out value))
{
limited.Status = serializer.ConvertToType<LimitedStatus>(value);
}
return limited;
}
public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
{
var limited = (Limited)obj;
if (limited == null)
return null;
var dict = new Dictionary<string, object>();
if (limited.Stuff != null)
dict.Add(StuffName, limited.Stuff);
if (limited.Status != null)
dict.Add(StatusName, limited.Status);
return dict;
}
public override IEnumerable<Type> SupportedTypes
{
get { return new [] { typeof(Limited) } ; }
}
}
And then use it like:
try
{
System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
serializer.RegisterConverters(new JavaScriptConverter[] { new LimitedConverter() });
var json = serializer.Serialize(new Limited());
Debug.WriteLine(json);
var status = serializer.Deserialize<Limited>(json);
var json2 = serializer.Serialize(status);
Debug.WriteLine(json2);
}
catch (Exception ex)
{
Debug.Assert(false, ex.ToString()); // NO ASSERT.
}
How can I deserialize the string based on what I have done in this method? Basically, what I have here is to pass the string through the network using serialization and deserialize the string in order to convey the message. But once I managed to receive the message, I have no idea if what I'm doing is correct. Here's the code:
string ConvertToString(FrogGame np, Frog1 pm, Frog2 pm2) //Serialization. the three parameters are the classes.
{
XmlSerializer sendSerializer = new XmlSerializer(typeof(FrogGame),new Type[]{typeof(Frog1),typeof(Frog2)});
StreamWriter myWriter = new StreamWriter(#"pad1.xml");
sendSerializer.Serialize(myWriter, np);
sendSerializer.Serialize(myWriter, pm);
sendSerializer.Serialize(myWriter, pm2);
return myWriter.ToString();
} //Overall, I serialize it into string
Once I pass the string through the network, I want to deserialize it in order the pass the message to the classes. How do I continue here onwards? How can I edit? The code:
void StringReceived(string str) //so str is myWriter.ToString()
{
XmlSerializer revSerializer = new XmlSerializer(typeof(FrogGame), new Type[] { typeof(Frog1), typeof(Frog2) });
FileStream myFileStream = new FileStream(#"pad1.xml", FileMode.Open);
FrogGame b = (FrogGame)revSerializer.Deserialize(myFileStream);
if (b is Frog1)
{
if (Network.IsServer())
{
pm = (Frog1)b;
pm.Position.Y = b.pm.Position.Y;
pm.Position.X = b.pm.Position.X;
}
else
{
System.Diagnostics.Debug.WriteLine("BAD Message: " + msg);
}
}
else if (b is Frog2)
{
if (Network.IsClient())
{
pm2 = (PaddleMessage2)b;
pm2.Position.Y = b.pm2.Position.Y;
pm2.Position.X = b.pm2.Position.X;
}
else
{
System.Diagnostics.Debug.WriteLine("BAD Message: " + msg);
}
}
}
I might misinterpret your problem, but I why don't you put all the thing you want to save in a class and do it like this (plus, if you use class, your data "transportation" and "management" will be much easier) :
SERIALIZATION
XmlSerializer serializer = new XmlSerializer(typeof(FrogGameData));
TextWriter textWriter = new StreamWriter("FrogGameSaveFile.xml");
serializer.Serialize(textWriter, _frogGameData);
textWriter.Close();
DESERIALIZATION
XmlSerializer deserializer = new XmlSerializer(typeof(FrogGameData));
TextReader textReader = new StreamReader("FrogGameSaveFile.xml");
_frogGameData = (FrogGameData)deserializer.Deserialize(textReader);
textReader.Close();
Note : The need-to-be-saved field should have property, because the tag in the XML will mimic the property name.
Additional Note : FrogGameData is not different than a normal class for automatic serialization like this. The XML will mimic your property order in the class for the one in the XML file.
But if you wanna need to rearrange the XML tag placement, you could do something like [XmlElement(Order = 1)],[XmlElement(Order = 2)], etc on top of your property to customize the order in XML file.
UPDATE
In case you need it, this is an example of your FrogGameData class :
public class FrogGameData
{
private Frog _frog1;
private Frog _frog2;
public Frog Frog1
{
get { return _frog1; }
set { _frog1 = value; }
}
public Frog Frog2
{
get { return _frog2; }
set { _frog2 = value; }
}
}
And the XML will pretty much like this :
<?xml version="1.0" encoding="utf-8"?>
<FrogGameData>
<Frog1>Something-depends-on-your-data</Frog1>
<Frog2>Something-depends-on-your-data</Frog2>
</FrogGameData>
But, if your class is (Note the XmlElement part) :
public class FrogGameData
{
private Frog _frog1;
private Frog _frog2;
[XmlElement(Order = 2)]
public Frog Frog1
{
get { return _frog1; }
set { _frog1 = value; }
}
[XmlElement(Order = 1)]
public Frog Frog2
{
get { return _frog2; }
set { _frog2 = value; }
}
}
Then, your XML will be :
<?xml version="1.0" encoding="utf-8"?>
<FrogGameData>
<Frog2>Something-depends-on-your-data</Frog2>
<Frog1>Something-depends-on-your-data</Frog1>
</FrogGameData>
I'm using XmlDictionaryWriter to serialize objects to a database with data contract serializer.
It works great, both size and speed are 2 times better then using text/xml.
However, I'll have to deal with enormous count of records in my database, where any extra bytes are directly translated into the gigabytes of the DB size.
That's why I'd love to reduce the size further, by using an XML dictionary.
How do I do that?
I see that XmlDictionaryWriter.CreateBinaryWriter static method accepts the 2-nd parameter of type IXmlDictionary. The MSDN says "The XmlDictionary to use as the shared dictionary".
First I've tried to use the system-supplied implementation:
XmlDictionary dict = new XmlDictionary();
string[] dictEntries = new string[]
{
"http://schemas.datacontract.org/2004/07/MyContracts",
"http://www.w3.org/2001/XMLSchema-instance",
"MyElementName1",
"MyElementName2",
"MyElementName3",
};
foreach ( string s in dictEntries )
dict.Add( s );
The result is .NET framework completely ignores the dictionary, and still inserts the above strings as plain text instead of just referencing a corresponding dictionary entry.
Then I've created my own implementation of IXmlDictionary:
class MyDictionary : IXmlDictionary
{
Dictionary<int, string> values = new Dictionary<int, string>();
Dictionary<string, int> keys = new Dictionary<string, int>();
MyDictionary()
{
string[] dictEntries = new string[]
{
"http://schemas.datacontract.org/2004/07/MyContracts",
"http://www.w3.org/2001/XMLSchema-instance",
"MyElementName1",
"MyElementName2",
"MyElementName3",
};
foreach ( var s in dictEntries )
this.Add( s );
}
static IXmlDictionary s_instance = new MyDictionary();
public static IXmlDictionary instance { get { return s_instance; } }
void Add( string val )
{
if ( keys.ContainsKey( val ) )
return;
int id = values.Count + 1;
values.Add( id, val );
keys.Add( val, id );
}
bool IXmlDictionary.TryLookup( XmlDictionaryString value, out XmlDictionaryString result )
{
if ( value.Dictionary == this )
{
result = value;
return true;
}
return this.TryLookup( value.Value, out result );
}
bool IXmlDictionary.TryLookup( int key, out XmlDictionaryString result )
{
string res;
if ( !values.TryGetValue( key, out res ) )
{
result = null;
return false;
}
result = new XmlDictionaryString( this, res, key );
return true;
}
public bool /* IXmlDictionary. */ TryLookup( string value, out XmlDictionaryString result )
{
int key;
if ( !keys.TryGetValue( value, out key ) )
{
result = null;
return false;
}
result = new XmlDictionaryString( this, value, key );
return true;
}
}
The result is - my TryLookup methods are called OK, however DataContractSerializer.WriteObject produces an empty document.
How do I use a pre-shared dictionary?
Thanks in advance!
P.S. I don't want to mess with XmlBinaryReaderSession/XmlBinaryWriterSession: I don't have "sessions", instead I have a 10 GB+ database accessed by many threads at once. What I want is just static pre-defined dictionary.
Update: OK I've figured out that I just need to call "XmlDictionaryWriter.Flush". The only remaining question is - why doesn't the system-supplied IXmlDictionary implementation work as expected?
for the XmlDictionaryWriter you need to use session.
example:
private static Stream SerializeBinaryWithDictionary(Person person,DataContractSerializer serializer)
{
var stream = new MemoryStream();
var dictionary = new XmlDictionary();
var session = new XmlBinaryWriterSession();
var key = 0;
session.TryAdd(dictionary.Add("FirstName"), out key);
session.TryAdd(dictionary.Add("LastName"), out key);
session.TryAdd(dictionary.Add("Birthday"), out key);
session.TryAdd(dictionary.Add("Person"), out key);
session.TryAdd(dictionary.Add("http://www.friseton.com/Name/2010/06"),out key);
session.TryAdd(dictionary.Add("http://www.w3.org/2001/XMLSchema-instance"),out key);
var writer = XmlDictionaryWriter.CreateBinaryWriter(stream, dictionary, session);
serializer.WriteObject(writer, person);
writer.Flush();
return stream;
}
The only way I way able to replicate the issue with the IXmlDictionary not being used was when my class wasn't decorated with a DataContract attribute. The following app displays the difference in sizes with decorated and undecorated classes.
using System;
using System.Runtime.Serialization;
using System.Xml;
namespace XmlPresharedDictionary
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Serialized sizes");
Console.WriteLine("-------------------------");
TestSerialization<MyXmlClassUndecorated>("Undecorated: ");
TestSerialization<MyXmlClassDecorated>("Decorated: ");
Console.ReadLine();
}
private static void TestSerialization<T>(string lineComment) where T : new()
{
XmlDictionary xmlDict = new XmlDictionary();
xmlDict.Add("MyElementName1");
DataContractSerializer serializer = new DataContractSerializer(typeof(T));
using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
using (var writer = XmlDictionaryWriter.CreateBinaryWriter(stream, xmlDict))
{
serializer.WriteObject(writer, new T());
writer.Flush();
Console.WriteLine(lineComment + stream.Length.ToString());
}
}
}
//[DataContract]
public class MyXmlClassUndecorated
{
public MyElementName1[] MyElementName1 { get; set; }
public MyXmlClassUndecorated()
{
MyElementName1 = new MyElementName1[] { new MyElementName1("A A A A A"), new MyElementName1("A A A A A") };
}
}
[DataContract]
public class MyXmlClassDecorated
{
public MyElementName1[] MyElementName1 { get; set; }
public MyXmlClassDecorated()
{
MyElementName1 = new MyElementName1[] { new MyElementName1("A A A A A"), new MyElementName1("A A A A A") };
}
}
[DataContract]
public class MyElementName1
{
[DataMember]
public string Value { get; set; }
public MyElementName1(string value) { Value = value; }
}
}