简体   繁体   中英

How can I serialize an object to C# object initializer code?

I'm looking to take an in-memory object (or JSON serialization of an object) and emit C# code to produce an equivalent object.

This would be useful for pulling known-good examples from a repository to use as starting points in unit tests. We have considered deserializing JSON, but C# code would have an edge when it comes to refactoring.

There is an interesting Visual Studio extension that addresses this; the Object Exporter . It allows serialization of an in-memory object into C# Object initialization code, JSON and XML. I haven't tried it yet but looks intriguing; will update after trying it out.

If your model is simple, you could use reflection and a string builder to output C# directly. I've done this to populate unit test data exactly as you discussed.

The code sample below was written in a few minutes and generated an object initializer that needed some hand tweaking. A more robust / less buggy function could be written if you plan on doing this a lot.

The second function is recursive, iterating over any Lists within the object, and generating code for those as well.

Disclaimer: This worked for my simple model with basic data types. It generated code that needed cleanup but allowed me to move on quickly. It is only here to serve as an example of how this could be done. Hopefully, it inspires someone to write their own.

In my case, I had an instance of this large dataset (results) that was loaded from the database. In order to remove the database dependency from my unit test, I handed the object to this function which spits out the code that allowed me to mock the object in my test class.

    private void WriteInstanciationCodeFromObject(IList results)
    {

        //declare the object that will eventually house C# initialization code for this class
        var testMockObject = new System.Text.StringBuilder();

        //start building code for this object
        ConstructAndFillProperties(testMockObject, results);

        var codeOutput = testMockObject.ToString();
    }


    private void ConstructAndFillProperties(StringBuilder testMockObject, IList results)
    {

        testMockObject.AppendLine("var testMock = new " + results.GetType().ToString() + "();");

        foreach (object obj in results)
        {

            //if this object is a list, write code for its contents

            if (obj.GetType().GetInterfaces().Contains(typeof(IList)))
            {
                ConstructAndFillProperties(testMockObject, (IList)obj);
            }

            testMockObject.AppendLine("testMock.Add(new " + obj.GetType().Name + "() {");

            foreach (var property in obj.GetType().GetProperties())
            {

               //if this property is a list, write code for its contents
                if (property.PropertyType.GetInterfaces().Contains(typeof(IList)))
                {
                    ConstructAndFillProperties(testMockObject, (IList)property.GetValue(obj, null));
                }

                testMockObject.AppendLine(property.Name + " = (" + property.PropertyType + ")\"" + property.GetValue(obj, null) + "\",");
            }

            testMockObject.AppendLine("});");
        }
    }

It's possible the object will have a TypeConverter that supports conversion to InstanceDescriptor , which is what the WinForms designer uses when emitting C# code to generate an object. If it can't convert to an InstanceDescriptor, it will attempt to use a parameterless constructor and simply set public properties. The InstanceDescriptor mechanism is handy, since it allows you to specify various construction options such as constructors with parameters or even static factory method calls.

I have some utility code I've written that emits loading of an in-memory object using IL, which basically follows the above pattern (use InstanceDescriptor if possible and, if not, simply write public properties.) Note that this will only produce an equivalent object if the InstanceDescriptor is properly implemented or setting public properties is sufficient to restore object state. If you're emitting IL, you can also cheat and read/write field values directly (this is what the DataContractSerializer supports), but there are a lot of nasty corner cases to consider.

I'm a novice at this as well, but I also needed to take a C# object that defined a hierarchy and extract it to an object initializer to ease setting up a unit test. I borrowed heavily from the above and wound up with this. I'd like to improve the way it handles recognizing user classes.

http://github.com/jefflomax/csharp-object-to-object-literal/blob/master/Program.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ObjectInitializer
{
    public class Program
    {
        public enum Color { Red, Green, Blue, Yellow, Fidget } ;

        public class Foo
        {
            public int FooId { get; set; }
            public string FooName { get; set; }
        }

        public class Thing
        {
            public int ThingId { get; set; }
            public string ThingName { get; set; }
            public List<Foo> Foos { get; set; }
        }

        public class Widget
        {
            public long Sort { get; set; }
            public char FirstLetter { get; set; }
        }

        public class TestMe
        {
            public Color Color { get; set; }
            public long Key { get; set; }
            public string Name { get; set; }
            public DateTime Created { get; set; }
            public DateTime? NCreated { get; set; }
            public bool Deleted { get; set; }
            public bool? NDeleted { get; set; }
            public double Amount { get; set; }
            public Thing MyThing { get; set; }
            public List<Thing> Things { get; set; }
            public List<Widget> Widgets { get; set; }
        }

        static void Main(string[] args)
        {
            var testMe = new TestMe
            {
                Color = Program.Color.Blue,
                Key = 3,
                Name = "SAK",
                Created = new DateTime(2013,10,20,8,0,0),
                NCreated = (DateTime?)null,
                Deleted = false,
                NDeleted = null,
                Amount = 13.1313,
                MyThing = new Thing(){ThingId=1,ThingName="Thing 1"},
                Things = new List<Thing>
                {
                    new Thing
                    {
                        ThingId=4,
                        ThingName="Thing 4",
                        Foos = new List<Foo>
                        {
                            new Foo{FooId=1, FooName="Foo 1"},
                            new Foo{FooId=2,FooName="Foo2"}
                        }
                    },
                    new Thing
                    {
                        ThingId=5,
                        ThingName="Thing 5",
                        Foos = new List<Foo>()
                    }
                },
                Widgets = new List<Widget>()
            };

            var objectInitializer = ToObjectInitializer(testMe);
            Console.WriteLine(objectInitializer);

            // This is the returned C# Object Initializer
            var x = new TestMe { Color = Program.Color.Blue, Key = 3, Name = "SAK", Created = new DateTime(2013, 10, 20, 8, 0, 0), NCreated = null, Deleted = false, NDeleted = null, Amount = 13.1313, MyThing = new Thing { ThingId = 1, ThingName = "Thing 1", Foos = new List<Foo>() }, Things = new List<Thing> { new Thing { ThingId = 4, ThingName = "Thing 4", Foos = new List<Foo> { new Foo { FooId = 1, FooName = "Foo 1" }, new Foo { FooId = 2, FooName = "Foo2" } } }, new Thing { ThingId = 5, ThingName = "Thing 5", Foos = new List<Foo>() } }, Widgets = new List<Widget>() };
            Console.WriteLine("");
        }

        public static string ToObjectInitializer(Object obj)
        {
            var sb = new StringBuilder(1024);

            sb.Append("var x = ");
            sb = WalkObject(obj, sb);
            sb.Append(";");

            return sb.ToString();
        }

        private static StringBuilder WalkObject(Object obj, StringBuilder sb)
        {
            var properties = obj.GetType().GetProperties();

            var type = obj.GetType();
            var typeName = type.Name;
            sb.Append("new " + type.Name + " {");

            bool appendComma = false;
            DateTime workDt;
            foreach (var property in properties)
            {
                if (appendComma) sb.Append(", ");
                appendComma = true;

                var pt = property.PropertyType;
                var name = pt.Name;

                var isList = property.PropertyType.GetInterfaces().Contains(typeof(IList));

                var isClass = property.PropertyType.IsClass;

                if (isList)
                {
                    IList list = (IList)property.GetValue(obj, null);
                    var listTypeName = property.PropertyType.GetGenericArguments()[0].Name;

                    if (list != null && list.Count > 0)
                    {
                        sb.Append(property.Name + " = new List<" + listTypeName + ">{");
                        sb = WalkList( list, sb );
                        sb.Append("}");
                    }
                    else
                    {
                        sb.Append(property.Name + " = new List<" + listTypeName + ">()");
                    }
                }
                else if (property.PropertyType.IsEnum)
                {
                    sb.AppendFormat("{0} = {1}", property.Name, property.GetValue(obj));
                }
                else
                {
                    var value = property.GetValue(obj);
                    var isNullable = pt.IsGenericType && pt.GetGenericTypeDefinition() == typeof(Nullable<>);
                    if (isNullable)
                    {
                        name = pt.GetGenericArguments()[0].Name;
                        if (property.GetValue(obj) == null)
                        {
                            sb.AppendFormat("{0} = null", property.Name);
                            continue;
                        }
                    }

                    switch (name)
                    {
                        case "Int64":
                        case "Int32":
                        case "Int16":
                        case "Double":
                        case "Float":
                            sb.AppendFormat("{0} = {1}", property.Name, value);
                            break;
                        case "Boolean":
                            sb.AppendFormat("{0} = {1}", property.Name, Convert.ToBoolean(value) == true ? "true" : "false");
                            break;
                        case "DateTime":
                            workDt = Convert.ToDateTime(value);
                            sb.AppendFormat("{0} = new DateTime({1},{2},{3},{4},{5},{6})", property.Name, workDt.Year, workDt.Month, workDt.Day, workDt.Hour, workDt.Minute, workDt.Second);
                            break;
                        case "String":
                            sb.AppendFormat("{0} = \"{1}\"", property.Name, value);
                            break;
                        default:
                            // Handles all user classes, should likely have a better way
                            // to detect user class
                            sb.AppendFormat("{0} = ", property.Name);
                            WalkObject(property.GetValue(obj), sb);
                            break;
                    }
                }
            }

            sb.Append("}");

            return sb;
        }

        private static StringBuilder WalkList(IList list, StringBuilder sb)
        {
            bool appendComma = false;
            foreach (object obj in list)
            {
                if (appendComma) sb.Append(", ");
                appendComma = true;
                WalkObject(obj, sb);
            }

            return sb;
        }
    }
}

I stumbled across this while looking for the same kind of method Matthew described, and was inspired by Evan's answer to write my own extension method. It generates compilable C# code as a string that can be copy/pasted into Visual Studio. I didn't bother with any particular formatting and just output the code on one line and use ReSharper to format it nicely. I've used it with some big DTOs that we were passing around and so far it works like a charm.

Here's the extension method and a couple helper methods:

public static string ToCreationMethod(this object o)
{
    return String.Format("var newObject = {0};", o.CreateObject());
}

private static StringBuilder CreateObject(this object o)
{
    var builder = new StringBuilder();
    builder.AppendFormat("new {0} {{ ", o.GetClassName());

    foreach (var property in o.GetType().GetProperties())
    {
        var value = property.GetValue(o);
        if (value != null)
        {
            builder.AppendFormat("{0} = {1}, ", property.Name, value.GetCSharpString());
        }
    }

    builder.Append("}");
    return builder;
}

private static string GetClassName(this object o)
{
    var type = o.GetType();

    if (type.IsGenericType)
    {
        var arg = type.GetGenericArguments().First().Name;
        return type.Name.Replace("`1", string.Format("<{0}>", arg));
    }

    return type.Name;
}

The method GetCSharpString contains the logic, and it's open to extension for any particular type. It was enough for me that it handled strings, ints, decimals, dates anything that implements IEnumerable:

private static string GetCSharpString(this object o)
{
    if (o is String)
    {
        return string.Format("\"{0}\"", o);
    }
    if (o is Int32)
    {
        return string.Format("{0}", o);
    }
    if (o is Decimal)
    {
        return string.Format("{0}m", o);
    }
    if (o is DateTime)
    {
        return string.Format("DateTime.Parse(\"{0}\")", o);
    }
    if (o is IEnumerable)
    {
        return String.Format("new {0} {{ {1}}}", o.GetClassName(), ((IEnumerable)o).GetItems());
    }

    return string.Format("{0}", o.CreateObject());
}

private static string GetItems(this IEnumerable items)
{
    return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + String.Format("{0}, ", item.GetCSharpString()));
}

I hope someone finds this useful!

It may comes a bit late, but here is my 5cents on that problem.

The mentioned Visual Studio Extension (OmarElabd/ObjectExporter) was a good idea, but I needed to generate C# code from in-memory objects at runtime, during unit test execution. This is what evolved from the original problem: https://www.nuget.org/packages/ObjectDumper.NET/

ObjectDumper.Dump(obj, DumpStyle.CSharp); returns C# initializer code from a variable. Please let me know if you find issues, you might want to report them on github.

There's a solution similar to what Evan proposed , but a bit better suited for my particular task.

After playing a bit with CodeDOM and Reflection it turned out that it would be too complicated in my case.

The object was serialized as XML, so the natural solution was to use XSLT to simply transform it to the object creation expression.

Sure, it covers only certain types of the cases, but maybe will work for someone else.

Here is an update to @revlucio's solution that adds support for booleans and enums.

public static class ObjectInitializationSerializer
{
    private static string GetCSharpString(object o)
    {
        if (o is bool)
        {
            return $"{o.ToString().ToLower()}";
        }
        if (o is string)
        {
            return $"\"{o}\"";
        }
        if (o is int)
        {
            return $"{o}";
        }
        if (o is decimal)
        {
            return $"{o}m";
        }
        if (o is DateTime)
        {
            return $"DateTime.Parse(\"{o}\")";
        }
        if (o is Enum)
        {
            return $"{o.GetType().FullName}.{o}";
        }
        if (o is IEnumerable)
        {
            return $"new {GetClassName(o)} \r\n{{\r\n{GetItems((IEnumerable)o)}}}";
        }

        return CreateObject(o).ToString();
    }

    private static string GetItems(IEnumerable items)
    {
        return items.Cast<object>().Aggregate(string.Empty, (current, item) => current + $"{GetCSharpString(item)},\r\n");
    }

    private static StringBuilder CreateObject(object o)
    {
        var builder = new StringBuilder();
        builder.Append($"new {GetClassName(o)} \r\n{{\r\n");

        foreach (var property in o.GetType().GetProperties())
        {
            var value = property.GetValue(o);
            if (value != null)
            {
                builder.Append($"{property.Name} = {GetCSharpString(value)},\r\n");
            }
        }

        builder.Append("}");
        return builder;
    }

    private static string GetClassName(object o)
    {
        var type = o.GetType();

        if (type.IsGenericType)
        {
            var arg = type.GetGenericArguments().First().Name;
            return type.Name.Replace("`1", $"<{arg}>");
        }

        return type.Name;
    }

    public static string Serialize(object o)
    {
        return $"var newObject = {CreateObject(o)};";
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM