Tuesday, May 10, 2011

Injecting XML Serialization for formatting decimal properties

When serializing objects that contain properties with type of decimal, the .NET XmlSerializer writes them to the resulting XML file exactly as they are specified in your program code. That means that if you assign a value of 5 then you will get the following XML from the serializer:

<Price>5</Price>

If you instead assign 5.00 you will receive the following output:

<Price>5.00</Price>

In this behavior the decimal type is different to all other numeric value types (e.g. double). For those of you who are interested at details you may have a look at comnumber.cpp which is the underlying Win32 API file used for formatting numbers. But that’s not the focus of this blog entry. I will just show one possibility to inject some additional behavior in order to format all the decimal properties you want formatted. This solution takes into account that you already have some code and objects you need to serialize and therefore tries to make as little changes to existing code as possible.

You can either tell the serializer to format all decimal properties it finds on an object or you may give it the chance to decide itself which properties to format and which not. The first approach requires no changes within your objects and is a lot easier. But if you want only certain properties to be formatted you must mark these properties accordingly. In order to do this we firstly define a custom attribute like follows:

[AttributeUsage(AttributeTargets.Property)]
public class DecimalFormatterAttribute : Attribute
{
    public DecimalFormatterAttribute(string formatString)
    {
        Format = formatString;
    }

    public string Format { get; private set; }
}

The attribute simply gets a user defined format string that will later be used to format the decimal value of the decorated property. With the AttributeUsage attribute we tell the compiler that our attribute is only valid on properties. With that attribute set up you can no go and decorate all the properties you want to be formatted by the serializer:

[DecimalFormatter("0.00")]
public decimal Price { get; set; }

To write out simple value types the XmlSerializer uses as type of XmlSerializationPrimitiveWriter. It would be the easiest way to extend this writer and use your own class for formatting. But due to its protection level you can’t do this. So the only thing one can do is to set the value of a given decimal property according to the format string specified within our custom property before serializing the object. That means that before we call XmlSerializer.Serialize(…) we need to format our decimal properties. There are a number of ways in which you can achieve this. I will present two of them here. Both solutions don’t require great code changes.

1. Custom serialization class

In a custom class you specify a Serialize and a Deserialize method. Both methods route their calls to an internal field of type XmlSerializer. In the Serialize method additional logic is executed before the call to XmlSerializer.Serialize:

public void Serialize(Stream stream, object o)
{
    var type = o.GetType();
    //get all public properties of o
    var properties = type.GetProperties();
    if (properties.Length > 0)
    {
        foreach (var propertyInfo in properties)
        {
            //apply only to properties of type decimal
            if (propertyInfo.PropertyType.Equals(typeof (decimal)))
            {
                //try to get DecimalFormatterAttribute attribute
                object[] formatterAttribute =
                    propertyInfo.GetCustomAttributes(
                                             typeof (DecimalFormatterAttribute), false);

                if (formatterAttribute.Length == 1)
                {
                    string format =
((DecimalFormatterAttribute)
formatterAttribute[0]).Format;
                    var value = (decimal) propertyInfo.GetValue(o, null);
                    var formattedString = value.ToString(
format, NumberFormatInfo.CurrentInfo); 

                    propertyInfo.SetValue(o,
decimal.Parse(formattedString), null);
                }
            }
        }
    }
    serializer.Serialize(stream, o);
} 

The method uses reflection to get all properties of the given object. If you don’t specify any BindingFlags value in the call to Type.GetProperties then all public properties will be retrieved by default. For each property – if it is of type decimal – we try to get our custom attribute and if we find it we apply it to the ToString method of the property value. Afterwards we set the property value by parsing the generated string. Of course this is kind of a little trick but I think it’s acceptable here. When we are finished formatting all decimal properties we simply need to call the Serialize method of the XmlSerializer.
The disadvantage of this solution is that you need to change all existing calls to XmlSerializer.Serialze you may already have in your application in that way that you must use a different class.

2. Use an extension method for XmlSerializer

If you don’t want a separate class encapsulating an XmlSerializer then you can use an extension method: 

public static class XmlSerializerExtension
{
    public static void SerializeDecimal(
this XmlSerializer serializer, Stream stream, object o)
    {
         
    }
} 

The method implementation is the same as above. Unfortunately you cannot name the extension method Serialize because the XmlSerializer class already contains that method. So slightly rename your method and you are able to use it instead of XmlSerializer.Serialize:

var s = new XmlSerializer(typeof (Product));
s.SerializeDecimal(stream, p);
 
Again you need to change at least the method call within your existing code. It’s purely a matter of taste which of the presented methods to choose.

4 comments:

  1. Could you share how to: "You can either tell the serializer to format all decimal properties it finds on an object"?

    ReplyDelete
    Replies
    1. You just need to call the Serialize method of the XmlSerializer to let the serializer decide itself how to serialize the properties. It will automatically serialize all public properties. So you don't need to use the techniques presented in this entry.

      Delete
    2. Thanks for the reply.

      Here's the scenario I have:

      I have an ORM (LLBLGen) that can either (based on config file setting) connect to an Oracle database or a SQL Server database for fetching data into some entity classes. I map this (ORM) entity into a class (generated from an .xsd using xsd.exe) that I use to serialize into XML (mapping each decimal field of the entity to a decimal field of the class)

      The problem is that, for Decimal types, for oracle, a decimal value of 66.00 is serializing as 66, but for sql server the value 66.00 is serializing as 66.00. For consistency and to keep from breaking unit and system tests, I need the results of the Oracle and the SQL Server tests to generate identically formatted output.

      In the debugger, when I look at a given decimal value, coming from the ORM, I see the SQL Server (decimal type)value shown as 66 and the Oracle value as 66. But I suspect there is some behind-the-scenes actual setting of a "to string" value that's held by the decimal.

      Here's another link I found to use the decimal.Round() function to try to force a specific format in the XmlSerializer, but it didn't seem to work for me. http://stackoverflow.com/questions/1613307/c-sharp-serialize-decimal-to-xml/13147418#13147418

      Thoughts?

      Delete
    3. That sounds kind of complicated and without knowing your code it's actually hard to tell what causes this behavior.

      As far as I understood you're serializing classes previously generated from an XSD. The classes get their data from corresponding ORM generated entity classes. You stated that inside the entity classes the values of decimal types coming from Oracle and SQL Server are the same (e.g. 66). After mapping them to the xsd classes, are they still the same? Or are they changed only after serializing? How's the mapping done? If you want to control the serialization process you may use the solution presented in the blog post. Otherwise you need to tweak the mapping process or try to find out when the values change.

      Delete