Serialize Nested Arrays To a CodeDOM Tree

Updated on 2019-07-26

A simple helper class to aid in code generation

Introduction

This is just a simple helper for serializing data structures to the CodeDOM. I'm providing it to the site as a copy and paste drop in class. All it does is take a data structure, including arrays and nested arrays, and create a CodeDOM expression tree that can be used to reinstantiate that data structure.

I use a variation of it in my code generation projects as it greatly aids with this task. In fact, for "table driven" code like parse tables and finite state machines, this makes code generation almost automatic.

Using the Code

All you need to do to use the code is call:

var exp=CodeDomUtility.Serialize(myValue);

And a CodeExpression of some kind is created which can be used to reinstantiate it. This works with scalar/primitive values and arrays, including nested arrays. It works with generic types but due to an apparent bug in Microsoft's VBCodeProvider, it won't generate generic types with arrays in them properly in all cases.

apparent bug

I'm providing the code below as copy paste because it's so simple that it doesn't really warrant a download.

using System;
using System.CodeDom;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.Reflection

// ... remember to put it in your namespace!

static partial class CodeDomUtility
{
    static CodeExpression _SerializeArray(Array arr)
    {
        if (1 == arr.Rank && 0 == arr.GetLowerBound(0))
        {
            var result = new CodeArrayCreateExpression( arr.GetType());
            foreach (var elem in arr)
                result.Initializers.Add(Serialize(elem));
            return result;
        }
        throw new NotSupportedException("Only SZArrays can be serialized to code.");
    }
    public static CodeExpression Serialize(object val)
    {
        if (null == val)
            return new CodePrimitiveExpression(null);
        if (val is bool ||
            val is string ||
            val is short ||
            val is ushort ||
            val is int ||
            val is uint ||
            val is ulong ||
            val is long ||
            val is byte ||
            val is sbyte ||
            val is float ||
            val is double ||
            val is decimal ||
            val is char)
        {
            return new CodePrimitiveExpression(val);
        }
        if (val is Array && 1 == ((Array)val).Rank && 0 == ((Array)val).GetLowerBound(0))
        {
            return _SerializeArray((Array)val);
        }
        var conv = TypeDescriptor.GetConverter(val);
        if (null != conv)
        {
            if (conv.CanConvertTo(typeof(InstanceDescriptor)))
            {
                var desc = conv.ConvertTo
                           (val, typeof(InstanceDescriptor)) as InstanceDescriptor;
                if (!desc.IsComplete)
                    throw new NotSupportedException(
                        string.Format(
                            "The type \"{0}\" could not be serialized.",
                            val.GetType().FullName));
                var ctor = desc.MemberInfo as ConstructorInfo;
                if (null != ctor)
                {
                    var result = new CodeObjectCreateExpression(ctor.DeclaringType);
                    foreach (var arg in desc.Arguments)
                        result.Parameters.Add(Serialize(arg));
                    return result;
                }
                throw new NotSupportedException(
                    string.Format(
                        "The instance descriptor for type \"{0}\" is not supported.",
                        val.GetType().FullName));
            }
            else
            {
                // we special case for KeyValuePair types.
                if (val.GetType().GetGenericTypeDefinition()==typeof(KeyValuePair<,>))
                {
                    // TODO: Find a workaround for the bug with VBCodeProvider
                    // may need to modify the reference source
                    var kvpType = new CodeTypeReference(typeof(KeyValuePair<,>));
                    foreach (var arg in val.GetType().GetGenericArguments())
                        kvpType.TypeArguments.Add(arg);
                    var result = new CodeObjectCreateExpression(kvpType);
                    for(int ic= kvpType.TypeArguments.Count,i = 0;i<ic;++i)
                    {
                        var prop = val.GetType().GetProperty(0==i?"Key":"Value");
                        result.Parameters.Add(Serialize(prop.GetValue(val)));
                    }
                    return result;
                }
                throw new NotSupportedException(
                    string.Format("The type \"{0}\" could not be serialized.",
                    val.GetType().FullName));
            }
        }
        else
            throw new NotSupportedException(
                string.Format(
                    "The type \"{0}\" could not be serialized.",
                    val.GetType().FullName));
    }
}

Just put this in your code and call it as shown before. You can use it as the InitExpression in (usually static) CodeMemberField to get it back as an instance.

Points of Interest

Finding the bug in Microsoft's code was disappointing.

Aside from that, note the use of InstanceDescriptor above in the code. This is so you can tell the serializer how to serialize things like classes and structs you make. Google it for more, since it's beyond the scope of this, but Microsoft uses it in their own code to support visual code design in devstudio.

History

  • 25th July, 2017 - Initial submission