1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
public class XmlStringSerializer { static Dictionary<string, Type> types = new Dictionary<string, Type>(); public string Serialize(object o) { StringBuilder sb = new StringBuilder(); Type t = o.GetType(); if (t.IsSerializable) { XmlSerializer serializer = new XmlSerializer(t); using (TextWriter tw = new StringWriter(sb)) { var namespaces = new XmlSerializerNamespaces(); namespaces.Add("", ""); serializer.Serialize(tw, o, namespaces); tw.Close(); } } else { throw new InvalidOperationException(string.Format("Objects of type {0} are not serializable.", t.Name)); } sb.Remove(0, 42); sb.Insert(0, '.'); sb.Insert(0, t.Namespace); sb.Insert(0, '<'); return sb.ToString(); } public T DeSerializeObject<T>(string content) { var typeName = content.Substring(1, content.IndexOf(' ', 20) - 1); Type type; if (!types.ContainsKey(typeName)) { type = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Single(t => t.FullName == typeName); lock (types) { types[typeName] = type; } } else { type = types[typeName]; } object o; var serializer = new XmlSerializer(type); int posAfterTypeName = content.IndexOf(' '); int posOfStartOfTypeName = content.LastIndexOf('.', posAfterTypeName) + 1; content = @"<?xml version=""1.0"" encoding=""utf-16""?><" + content.Substring(posOfStartOfTypeName); using (TextReader tr = new StringReader(content)) { o = serializer.Deserialize(tr); tr.Close(); } return (T) o; } }
Refactorings
No refactoring yet !
Moonshield
January 22, 2009, January 22, 2009 03:45, permalink
Unfortunately, your code wasn't working for me. I think that my default xml encoding differs from yours so that your hard-coded string manipulation isn’t working properly. On the other side, I worked on a cleaner way to achieve what you want. It can serializes an object of type A, then deserializes it back to the same type or to an object of type B : A like you mentioned. The key of the implementation is to use a general root name (see XmlRoot in my code) for both objects. Enough talking, see the code below :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
using System; using System.IO; using System.Xml.Serialization; namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { // Serialize a Person object and deserialize it back to a Person or an Employee string SerializedObject = SerializedObject = CustomSerializer.Serialize(new Person() { Id = 1, FirstName = "Peter", LastName = "Pan" }); Person oPerson = CustomSerializer.DeSerialize<Person>(SerializedObject); Employee oEmployee = CustomSerializer.DeSerialize<Employee>(SerializedObject); // At this point the Employee.Title is empty, everything's normal. Then we will // serialize the same Employee object and deserialize it back to a Person object oEmployee.Title = "Hero"; SerializedObject = CustomSerializer.Serialize(oEmployee); oPerson = CustomSerializer.DeSerialize<Person>(SerializedObject); Console.ReadLine(); } } /// <summary> /// Contains information about a person. /// </summary> /// <remarks> /// The XmlRoot attribute allow to serialize a Person object /// and to deserialize it back to a Person or an Employee /// object without "manipulating" the xml string. /// </remarks> [Serializable, XmlRoot("Base")] public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } /// <summary> /// Contains information about an employee. /// </summary> /// <remarks> /// The XmlRoot attribute allow to serialize a Person object /// and to deserialize it back to a Person or an Employee /// object without "manipulating" the xml string. /// </remarks> [Serializable, XmlRoot("Base")] public class Employee : Person { public string Title { get; set; } } /// <summary> /// /// </summary> public class CustomSerializer { /// <summary> /// Serialize an object to xml /// </summary> /// <param name="pSource">Object Type</param> /// <returns>String</returns> public static string Serialize(object pSource) { // Normally I'd use this 4 lines but since you need cleaner xml see below //StringWriter oWriter = new StringWriter(); //XmlSerializer oSerializer = new XmlSerializer(pSource.GetType()); //oSerializer.Serialize(oWriter, pSource); //return oWriter.ToString(); XmlSerializerNamespaces oNamespaces = new XmlSerializerNamespaces(); oNamespaces.Add("", ""); StringWriter oWriter = new StringWriter(); XmlSerializer oSerializer = new XmlSerializer(pSource.GetType()); oSerializer.Serialize(oWriter, pSource, oNamespaces); string Serialized = oWriter.ToString(); return Serialized.Remove(0, Serialized.IndexOf('<', 1)); } /// <summary> /// Deserialize an xml string to an object /// </summary> /// <param name="pXmlSource">Xml Source</param> /// <returns>Deserialized object</returns> public static T DeSerialize<T>(string pXmlSource) { return (T)(new XmlSerializer(typeof(T)).Deserialize(new StringReader(pXmlSource))); } } }
mcintyre321
January 22, 2009, January 22, 2009 10:28, permalink
Good effort but I'm afraid it doesn't quite fit the bill of what I'm looking for (I should have been a bit clearer about the requirements)! I actually need to have the object B returned as type B, even though it is stored in a field reference of type A. I've rewritten my code so it a/ should work better and b/ has a unit tests. The tests should stay the same
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml.Serialization; using NUnit.Framework; namespace XmlSerializerTests { namespace XmlSerializerTests.SomeOtherNamespace { [Serializable] public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } } [Serializable] public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } [Serializable] public class Employee : Person { public string Title { get; set; } } [TestFixture] public class Tests { CustomSerializer serializer = new CustomSerializer(); [Test] public void TestThatAClassCanBeDeserializedAsItsBaseClass() { var employee = new Employee() {FirstName = "John", LastName = "Doe", Id = 1, Title = "Dogsbody"}; var xml = serializer.Serialize(employee); var obj = serializer.DeSerializeObject<Person>(xml); Assert.AreEqual(typeof (Employee), obj.GetType()); } [Test, ExpectedException(typeof(InvalidCastException))] public void NoDuckSerializationTest() { var person = new Person() { FirstName = "John", LastName = "Doe", Id = 1}; var xml = serializer.Serialize(person); var someOtherPerson = serializer.DeSerializeObject<XmlSerializerTests.SomeOtherNamespace.Person>(xml); } } public class CustomSerializer { static Dictionary<string, Type> types = new Dictionary<string, Type>(); public string Serialize(object o) { StringBuilder sb = new StringBuilder(); Type t = o.GetType(); if (t.IsSerializable) { XmlSerializer serializer = new XmlSerializer(t); using (TextWriter tw = new StringWriter(sb)) { var namespaces = new XmlSerializerNamespaces(); namespaces.Add("", ""); serializer.Serialize(tw, o, namespaces); tw.Close(); } } else { throw new InvalidOperationException(string.Format("Objects of type {0} are not serializable.", t.Name)); } sb.Remove(0, 42); sb.Insert(0, '.'); sb.Insert(0, t.Namespace); sb.Insert(0, '<'); sb.Remove(sb.Length - t.Name.Length - 2, t.Name.Length + 2); sb.Append(t.FullName); sb.Append(@"/>"); return sb.ToString(); } public T DeSerializeObject<T>(string content) { var indexOfFirstSpace = content.IndexOf(' ', 20); var indexOfFirstBreak = content.IndexOf("\r\n"); var indexOfFirstCloseBrace = content.IndexOf(">"); var endOfTypeNameIndex = new[] {indexOfFirstSpace, indexOfFirstBreak, indexOfFirstCloseBrace}.Where(i => i > -1).Min(); var typeName = content.Substring(1, endOfTypeNameIndex - 1).Trim(); Type type; if (!types.ContainsKey(typeName)) { type = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).Single(t => t.FullName == typeName); lock (types) { types[typeName] = type; } } else { type = types[typeName]; } object o; var serializer = new XmlSerializer(type); int posAfterTypeName = content.IndexOf(' '); int posOfStartOfTypeName = content.LastIndexOf('.', posAfterTypeName) + 1; content = @"<?xml version=""1.0"" encoding=""utf-16""?><" + content.Substring(posOfStartOfTypeName); content = content.Substring(0, content.Length - typeName.Length - 2) + '/' + type.Name + '>'; using (TextReader tr = new StringReader(content)) { o = serializer.Deserialize(tr); tr.Close(); } return (T)o; } } }
The default XML serializer does some things I consider undesirable
1. The XML it produces is full of unnecessary XML Namespace stuff
2. It is not polymorphic - say you have class B : A { }, and have serialized a type B, you should be able to call Deserialize<A>() on it
My implementation corrects these issues, but it seems a bit crazy!