4
\$\begingroup\$

I have recently implemented this utility class for going through an instance's member fields recursively and assigning them default values if they are null. Here is the implementation in the NullHandler class:

public static class NullHandler
{
    public static void PopulateNullFields<T>(T obj)
    {
        var fields = obj.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic);

        foreach(var field in fields)
        {
            var defaultValue = field.GetValue(obj);

            if(defaultValue == null)
            {
                defaultValue = GetDefaultValue(field.FieldType);
            }

            var subFields = field.FieldType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic);

            if(subFields.Count() > 0 && !field.FieldType.IsValueType && field.FieldType != typeof(string))
            {
                PopulateNullFields(defaultValue);
            }

            field.SetValue(obj, defaultValue);
        }
    }

    public static object GetDefaultValue(Type t)
    {
        if(Nullable.GetUnderlyingType(t) != null)
        {
            t = Nullable.GetUnderlyingType(t);
        }

        if(t == typeof(string))
        {
            return "";
        }
        else if(t == typeof(DateTime))
        {
            return new DateTime();
        }
        else if (t.IsValueType)
        {
            return Activator.CreateInstance(t);
        }
        else if(t.IsArray)
        {
            var cinfo = t.GetConstructors().FirstOrDefault(x => x.GetParameters().Count() == 1);

            if(cinfo != null)
            {
                return cinfo.Invoke(new object[] { 0 });
            }
            else
            {
                throw new InvalidOperationException("Unable to find array constructor for this array type.");
            }
        }
        else if(t.IsEnum)
        {
            return Enum.GetValues(t).GetValue(0);
        }
        else
        {
            var cinfo = t.GetConstructors().Where(x => x.GetParameters().Count() == 0).FirstOrDefault();
            if (cinfo != null)
            {
                return Activator.CreateInstance(t);
            }
            else
            {
                throw new InvalidOperationException("No default constructor with no parameter is found for the type.");
            }
        }
    }
}

The code is currently in use in our XML serialization routine, where we have a requirement for another system that expects an XML file with null values included. The .NET XmlSerializer class omits null properties in the generated XML unless the properties are decorated with the XmlElement(IsNullable = true) attribute.

While we would have rather added the attributes to the model class itself, the model class is (a) created from code-generation, and we would lose the changes if we needed to regenerate the model class, and (b) the generated model class is at least ~3000 LOC, making it time-consuming to manually add the needed attributes ourselves.

Here is a sample of how the NullHandler utility class is in use on our project:

using (var stringWriter = new StringWriter())
using (var xmlStringWriter = XmlWriter.Create(stringWriter, settings))
{
     NullHandler.PopulateNullFields(item.Result.Payee);
     serializer.Serialize(xmlStringWriter, item.Result);

     // ...save XML to database
}

I would like to get feedback if there are areas where the code can be improved. I would specifically like to know how I can improve the reusability of the NullHandler class by accounting for any possibly missed cases (most especially in the GetDefaultValues method). Feedback to improve the class's readability would be appreciated as well.

\$\endgroup\$
  • 2
    \$\begingroup\$ Fixing the model class generator itself seems cleaner. \$\endgroup\$ – vnp May 1 '18 at 5:36
  • \$\begingroup\$ We used the xsd.exe tool from Microsoft to generate the model classes. The XSD file came from a third-party. So we didn't really have the option to modify how the tool generated the model class. \$\endgroup\$ – InstilledBee May 1 '18 at 10:35
  • 1
    \$\begingroup\$ Why are you setting private fields instead of public fields and properties? If that doesn't break anything right now, how certain are you that it won't cause problems later, when the generated code needs to be updated? \$\endgroup\$ – Pieter Witvoet May 1 '18 at 20:39
  • \$\begingroup\$ Thanks for pointing that out. I have assumed in this case that directly modifying the private fields was appropriate given that xsd.exe created backing fields for the properties anyway. I'll consider this when I go through the code again. \$\endgroup\$ – InstilledBee May 2 '18 at 12:56
2
\$\begingroup\$

You can replace some parts (like enums, value types, or date-time) of the GetDefaultValue method with Expression.Default which

Represents the default value of a type or an empty expression.

This means that if you wanted to get the default value for DateTime you could use Expression.Default to generate this code for you: (object)default(DateTime).

And it goes like that:

var type = typeof(DateTime);

// (object)default(type)
var getDefaultValue =
    Expression.Lambda<Func<object>>(
        // it needs to be casted to `object` to create `Func<object>`
        Expression.Convert(
            Expression.Default(type),
            typeof(object)
        )
    ).Compile();

getDefaultValue().Dump(); // 01/01/0001 00:00:00

Just make sure to cache the generated Func<object> for each Type because Compile takes some time and it will otherwise become the bottleneck of your utility.


"No default constructor with no parameter is found for the type.";

A default constructor is a parameterless one so it's not necessary to repeat it.

\$\endgroup\$
  • \$\begingroup\$ Accepting this answer because this was the one that provided direct feedback on the code I posted and how it could be improved. Thanks. \$\endgroup\$ – InstilledBee May 3 '18 at 12:31
3
\$\begingroup\$

A different option is to fix this at the creation of the XmlSerializer. The XmlSerializer constructor can take a parameter for XmlAttributeOverrides

You could create a method to auto add the XmlElementAttribute, if missing, or set the IsNullable to true.

public static XmlAttributeOverrides AddIsNullableTrue(Type type)
{
    var xmlOverrides = new XmlAttributeOverrides();
    foreach (
        var prop in
            type.GetProperties()
                .Where(p => !p.PropertyType.IsValueType || p.PropertyType == typeof (string)))
    {
        var xmlAttributes = new XmlAttributes();
        var attr = prop.GetCustomAttribute<XmlElementAttribute>();
        if (attr != null)
        {
            attr.IsNullable = true;
        }
        else
        {
            attr = new XmlElementAttribute() {IsNullable = true};
        }
        xmlAttributes.XmlElements.Add(attr);
        xmlOverrides.Add(type, prop.Name, xmlAttributes);
    }
    return xmlOverrides;
}

Now when you create your XmlSerializer you just need to pass in the XmlAttributeOverrides

XmlSerializer SerializerObj = new XmlSerializer(typeof(TestClass), AddIsNullableTrue(typeof(TestClass)));

This example is assuming the class is called TestClass. Now you don't need to populate null fields and fields will be output to XML that are null.

\$\endgroup\$
  • \$\begingroup\$ I wasn't able to find out about XmlAttributeOverrides when initially searching for strategies to our problem. The solution I posted was the one I quickly thought of given a quick deadline on the task as well. Thanks for posting a cleaner solution. \$\endgroup\$ – InstilledBee May 3 '18 at 12:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.