Using Roslyn to build object to object mapper

Back in time I wrote series of posts about how I built simple object to object mapper. It was nine years ago and from them more things have changed. Today I added one new implementation of mapper and this one is using Roslyn compiler services to generate dynamic code for mappings.

Dynamic code in current era

As I’m new to compiler services I found some great starting materials:

Based on these two sources I implemented new dynamic code based mapper.

For those in hurry here is my base class for mapper implementations.

public abstract class ObjectCopyBase
{
    public abstract void MapTypes(Type source, Type target);
    public abstract void Copy(object source, object target);

    protected virtual IList<PropertyMap> GetMatchingProperties(Type sourceType, Type targetType)
    {
        var sourceProperties = sourceType.GetProperties();
        var targetProperties = targetType.GetProperties();

        var properties = (from s in sourceProperties
                            from t in targetProperties
                            where s.Name == t.Name &&
                                s.CanRead &&
                                t.CanWrite &&
                                s.PropertyType == t.PropertyType
                            select new PropertyMap
                            {
                                SourceProperty = s,
                                TargetProperty = t
                            }).ToList();
        return properties;
    }

    protected virtual string GetMapKey(Type sourceType, Type targetType)
    {
        var keyName = "Copy_";
        keyName += sourceType.FullName.Replace(".", "_").Replace("+", "_");
        keyName += "_";
        keyName += targetType.FullName.Replace(".", "_").Replace("+", "_");

        return keyName;
    }
}

It provides some general functionalities like matching assignable properties of source and target type and providing dictionary key based on types. It also defines two abstract methods for adding types to type map and copying properties from one object to another.

Generating dynamic code

Dynamic code mapper generates mapping code on the run. When mappings code is done it prepares reference assemblies, builds the code and caches mapping type.

Generating dynamic code for object to object mapping

Here’s the code I wrote for dynamic code generation. I added comments to make code easier to understand.

public class MapperDynamicCode : ObjectCopyBase
{
    private readonly Dictionary<string, Type> _comp = new Dictionary<string, Type>();

    public override void MapTypes(Type source, Type target)
    {
        var key = GetMapKey(source, target);
        if (_comp.ContainsKey(key))
        {
            return;
        }

        // Create mapping code
        var builder = new StringBuilder();
        builder.AppendLine("using ObjectToObjectMapper;\r\n");
        builder.Append("namespace Copy {\r\n");
        builder.Append("    public class ");
        builder.Append(key);
        builder.Append(" {\r\n");
        builder.Append("        public static void CopyProps(");
        builder.Append(target.FullName.Replace("+", "."));
        builder.Append(" source, ");
        builder.Append(target.FullName.Replace("+", "."));
        builder.Append(" target) {\r\n");

        var map = GetMatchingProperties(source, target);
        foreach (var item in map)
        {
            builder.Append("            target.");
            builder.Append(item.TargetProperty.Name);
            builder.Append(" = ");
            builder.Append("source.");
            builder.Append(item.SourceProperty.Name);
            builder.Append(";\r\n");
        }

        builder.Append("        }\r\n   }\r\n}");           

        // Prepare reference assemblies
        string assemblyName = Path.GetRandomFileName();
        var refPaths = new[] {
            typeof(Object).GetTypeInfo().Assembly.Location,
            typeof(Console).GetTypeInfo().Assembly.Location,
            Path.Combine(Path.GetDirectoryName(typeof(GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"),
            GetType().GetTypeInfo().Assembly.Location
        };

        var references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray();
        var syntaxTree = CSharpSyntaxTree.ParseText(builder.ToString());

        // Compile dynamic code
        var compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

        using(var ms = new MemoryStream())
        {
            // Emit to in-memory assembly
            var result = compilation.Emit(ms);
            ms.Seek(0, SeekOrigin.Begin);

            // Load assembly from memory
            var assembly = AssemblyLoadContext.Default.LoadFromStream(ms);

            // Get mapper type
            var type = assembly.GetType("Copy." + key);

            // Add mapper type to type cache
            _comp.Add(key, type);
        }
    }

    public override void Copy(object source, object target)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();

        // Get or create mapping if missing
        var key = GetMapKey(sourceType, targetType);
        if (!_comp.ContainsKey(key))
        {
            MapTypes(sourceType, targetType);
        }

        // Prepare mapping call
        var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod;
        var args = new[] { source, target };

        // Execute mapping
        _comp[key].InvokeMember("CopyProps", flags, null, null, args);
    }
}

Copy() method is simple one. It gets mapping if available or creates one if missing and then makes call to CopyProps() method using reflection.

Using Lightweight Code Generation for dynamic code

I also updated mapper that uses Lightweight Code Generation (LCG). It creates dynamic code by emitting Intermediate Language (IL) instructions to memory.

public class MapperLcg : ObjectCopyBase
{
    private readonly Dictionary<string, DynamicMethod> _del = new Dictionary<string, DynamicMethod>();

    public override void MapTypes(Type source, Type target)
    {
        var key = GetMapKey(source, target);
        if (_del.ContainsKey(key))
        {
            return;
        }

        var args = new[] { source, target };
        var mod = typeof(Program).Module;

        var dm = new DynamicMethod(key, null, args, mod);
        var il = dm.GetILGenerator();
        var maps = GetMatchingProperties(source, target);

        foreach (var map in maps)
        {
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldarg_0);
            il.EmitCall(OpCodes.Callvirt, map.SourceProperty.GetGetMethod(), null);
            il.EmitCall(OpCodes.Callvirt, map.TargetProperty.GetSetMethod(), null);
        }
        il.Emit(OpCodes.Ret);
        _del.Add(key, dm);
    }

    public override void Copy(object source, object target)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
        var key = GetMapKey(sourceType, targetType);

        var del = _del[key];
        var args = new[] { source, target };
        del.Invoke(null, args);
    }
}

The trick here is to use DynamicMethod class to create new method that does actual mapping.

Benchmarking dynamic code

Here is the simple benchmark I wrote. It takes average time to perform mapping using one million cycles.

public class OrderModel
{
    public int Id { get; set; }
    public string CustomerName { get; set; }
    public string DeliveryAddress { get; set; }
    public string OrderReference { get; set; }
    public DateTime EstimatedDeliveryDate { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var source = new OrderModel
        {
            Id = 1,
            CustomerName = "John Doe",
            DeliveryAddress = "Lonely Souls Blvd. 1382",
            EstimatedDeliveryDate = DateTime.Now,
            OrderReference = "ODF/SDP/1929242111-237821"
        };
        var target = new OrderModel();

        TestMappers(source, target);

        Console.WriteLine(Environment.NewLine);
        Console.WriteLine("Press any key to exit ...");
        Console.ReadKey();
    }

    static void TestMappers(object source, object target)
    {
        var mappers = new ObjectCopyBase[]
                            {
                                new MapperDynamicCode(),
                                new MapperLcg()
                            };

        var sourceType = source.GetType();
        var targetType = target.GetType();
        var stopper = new Stopwatch();
        var testRuns = 1000000;

        foreach (var mapper in mappers)
        {
            mapper.MapTypes(sourceType, targetType);

            stopper.Restart();

            for (var i = 0; i < testRuns; i++)
            {
                mapper.Copy(source, target);
            }

            stopper.Stop();

            var time = stopper.ElapsedMilliseconds / (double)testRuns;
            Console.WriteLine(mapper.GetType().Name + ": " + time);
        }
    }
}

Times are shown in milliseconds.

What has happened over time?

When I first played with object to object mapper I also measured performance of different implementations. The fastest one back in time was mapper that generated Intermedia Language instructions using Lightweight Code Generation. I made it also use generics to boost up its speed.

I tested implementations using simple data transfer object (DTO) and took average copying time over million iterations. Back in time I also had dynamic code implementation that used CodeDOM to build mapping assemblies.

Here’s the table that compares benchmarks done back in time and now. LCG code is the same. Old dynamic code used CodeDOM and new one uses Roslyn.

ImplementationOld (ms)New (ms)
LCG0.00190.0013
Dynamic code0.00580.0014

It’s fancy to see that dynamic code built with Roslyn and run on .NET Core performs same well as LCG.

Devil in details. There is actually on big difference in timings between Roslyn and LCG. Creating new mapping class directly to memory is way faster with LCG version. In my tests Roslyn dynamic code version always stopped for about second to build and load the dynamic assembly.

Wrapping up

I got back to my old experiment because of article I’m writing for one big and famous blog. I wrote this post because it was surprising for me to see how much things have improved over nine years and what excellent tooling we are using today. Roslyn seems very interesting beast to me and it’s definitely worth some deeper exploring. Putting these things together I see many new fronts for well performing dynamic code that is not hard to implement.

Liked this post? Empower your friends by sharing it!

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    3 thoughts on “Using Roslyn to build object to object mapper

    • September 18, 2019 at 3:29 pm
      Permalink

      Nice article Gunnar. How does this mapper compare to the current AutoMapper?

    • September 19, 2019 at 8:07 am
      Permalink

      Thanks! Back in time my mapping code was way faster than AutoMapper but it has changed. AutoMapper has long history and over time things have improved heavily. Current AutoMapper is faster than this code and I still have to find the trick.

    • September 19, 2019 at 10:56 pm
      Permalink

      Over a year ago I created MappingGenerator project. It’s a VisualStudio plugin that allows generating mapping code in design time. Rather than reflection and string concatenation, it utilizes Roslyn API for analyzing and generating AST. The whole source code and documentation are available on the GitHub project site https://github.com/cezarypiatek/MappingGenerator This project is a response to all the downsides of generating code in the runtime.

    Leave a Reply

    Your email address will not be published. Required fields are marked *