views:

1062

answers:

3

I am looking for a way to convert an object tree to XML. It would be fun to write, but I am sure someone has already written it. Here is my wish list:

  • It should not care about constructors
  • It should ideally handle circular references (not too fussed how)
  • It should not require changes to the objects - e.g., no custom attributes
  • It should not care about or require known types (e.g., XmlInclude)
  • The XML should be dead simple - it needs to be human readable by members of the operations team
  • If a property can't be serialized, it should just suppress the error and continue
  • Can handle lists and dictionaries

I don't need to reconstruct the object model, so a write-only solution is fine (probably expected).

I think that discounts:

  • XmlSerializer - needs parameterless constructors, no circular reference support
  • DataContractSerializer - needs attributes (opt in)
+1  A: 

I doubt you're going to find anything that works particularly well across all classes. As you pointed out, XmlSerializer is Microsoft's best effort at the generic end of things so far.

On the other end are visualizers, which are unique to a specific class. I don't think there's much of a happy medium yet.

Jekke
+3  A: 

This seems like it would be straightforward to write using reflection: given an object instance, create an XML element with its class name, and then iterate through all of its properties.

For each property create an element with its name:

  • if it's a value type, set its text to the XML Schema text of its value;
  • if it implements IEnumerable, iterate through it and create an element for each item;
  • if it's any other reference type, set the element's content to the property's XML representation.

Track circular/multiple references with a HashSet containing the hash codes of each object you've serialized; if you find an object's hash code in the HashSet, you've already serialized it. (I don't know what you want put into the XML if this happens.)

But no, I don't have any code that does this lying around.

Robert Rossney
+3  A: 

Robert Rossney's post made me think it's probably less work than I thought. So here's a very rough attempt. It handles the following:

  • If it is unable to read a property, it prints the exception as the value
  • Cyclic references and multiple occurrences. It associates an ID with each element; if an element appears twice, it just points the ref ID. The Ref ID is unique to the object graph (I should probably use a GUID, but this suits my purposes).
  • It has no problems with derived types
  • It requires no attributes or specific constructors or other nonsense
  • It can handle read-only properties

Here's an example of the output (in my test objects, the "Currency" product on the Order throws an exception).

<Customer Ref="1">
  <FirstName>Paul</FirstName>
  <LastName>Stovell</LastName>
  <FullName>Paul Stovell</FullName>
  <Orders>
    <Order Ref="2">
      <SKU>Apples</SKU>
      <Price>27.30</Price>
      <Currency>Something bad happened</Currency>
      <Customer Ref="1" />
    </Order>
    <Order Ref="3">
      <SKU>Pears</SKU>
      <Price>17.85</Price>
      <Currency>Something bad happened</Currency>
      <Customer Ref="1" />
    </Order>
    <Order Ref="2" />
  </Orders>
</Customer>

Here's the sample object model and usage:

static void Main(string[] args)
{
    var customer = new Customer();
    customer.FirstName = "Paul";
    customer.LastName = "Stovell";
    customer.Orders.Add(new Order(customer) { Price = 27.30M, SKU = "Apples"});
    customer.Orders.Add(new Order(customer) { Price = 17.85M, SKU = "Pears"});
    customer.Orders.Add(customer.Orders[0]);

    var output = new StringWriter();
    var writer = new XmlTextWriter(output);
    writer.Formatting = Formatting.Indented;
    WriteComplexObject("Customer", customer, writer);
    Console.WriteLine(output.ToString());
    Console.ReadKey();
}

class Customer
{
    private readonly List<Order> _orders = new List<Order>();

    public Customer()
    {
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
    {
        // Read-only property test
        get { return FirstName + " " + LastName; }
    }

    public List<Order> Orders
    {
        // Collections test
        get { return _orders; }
    }
}

class Order
{
    private readonly Customer _customer;

    public Order(Customer customer)
    {
        _customer = customer;
    }

    public string SKU { get; set; }
    public decimal Price { get; set; }
    public string Currency
    {
        // A proprty that, for some reason, can't be read
        get
        {
            throw new Exception("Something bad happened");
        }
    }

    public Customer Customer
    {
        get { return _customer; }
    }
}

Here's the implementation:

public static void WriteObject(string name, object target, XmlWriter writer)
{
    WriteObject(name, target, writer, new List<object>(), 0, 10, -1);
}

private static void WriteObject(string name, object target, XmlWriter writer, List<object> recurringObjects, int depth, int maxDepth, int maxListLength)
{
    var formatted = TryToFormatPropertyValueAsString(target);
    if (formatted != null)
    {
        WriteSimpleProperty(name, formatted, writer);
    }
    else if (target is IEnumerable)
    {
        WriteCollectionProperty(name, (IEnumerable)target, writer, depth, maxDepth, recurringObjects, maxListLength);
    }
    else
    {
        WriteComplexObject(name, target, writer, recurringObjects, depth, maxDepth, maxListLength);
    }
}

private static void WriteComplexObject(string name, object target, XmlWriter writer, List<object> recurringObjects, int depth, int maxDepth, int maxListLength)
{
    if (target == null || depth >= maxDepth) return;
    if (recurringObjects.Contains(target))
    {
        writer.WriteStartElement(name);
        writer.WriteAttributeString("Ref", (recurringObjects.IndexOf(target) + 1).ToString());
        writer.WriteEndElement();
        return;
    }
    recurringObjects.Add(target);

    writer.WriteStartElement(name);
    writer.WriteAttributeString("Ref", (recurringObjects.IndexOf(target) + 1).ToString());
    foreach (var property in target.GetType().GetProperties())
    {
        var propertyValue = ReadPropertyValue(target, property);
        WriteObject(property.Name, propertyValue, writer, recurringObjects, depth + 1, maxDepth, maxListLength);
    }
    writer.WriteEndElement();
}

private static object ReadPropertyValue(object target, PropertyInfo property)
{
    try { return property.GetValue(target, null); }
    catch (Exception ex) { return ReadExceptionMessage(ex); }
}

private static string ReadExceptionMessage(Exception ex)
{
    if (ex is TargetInvocationException && ex.InnerException != null)
        return ReadExceptionMessage(ex.InnerException);
    return ex.Message;
}

private static string TryToFormatPropertyValueAsString(object propertyValue)
{
    var formattedPropertyValue = null as string;
    if (propertyValue == null)
    {
        formattedPropertyValue = string.Empty;
    }
    else if (propertyValue is string || propertyValue is IFormattable || propertyValue.GetType().IsPrimitive)
    {
        formattedPropertyValue = propertyValue.ToString();
    }
    return formattedPropertyValue;
}

private static void WriteSimpleProperty(string name, string formattedPropertyValue, XmlWriter writer)
{
    writer.WriteStartElement(name);
    writer.WriteValue(formattedPropertyValue);
    writer.WriteEndElement();
}

private static void WriteCollectionProperty(string name, IEnumerable collection, XmlWriter writer, int depth, int maxDepth, List<object> recurringObjects, int maxListLength)
{
    writer.WriteStartElement(name);
    var enumerator = null as IEnumerator;
    try
    {
        enumerator = collection.GetEnumerator();
        for (var i = 0; enumerator.MoveNext() && (i < maxListLength || maxListLength == -1); i++)
        {
            if (enumerator.Current == null) continue;
            WriteComplexObject(enumerator.Current.GetType().Name, enumerator.Current, writer, recurringObjects, depth + 1, maxDepth, maxListLength);
        }
    }
    catch (Exception ex)
    {
        writer.WriteElementString(ex.GetType().Name, ReadExceptionMessage(ex));
    }
    finally
    {
        var disposable = enumerator as IDisposable;
        if (disposable != null)
        {
            disposable.Dispose();
        }
        writer.WriteEndElement();
    }
}

I would still be interested to know if there are more tried and tested solutions.

Paul Stovell