Building a Blazing Fast Object-to-Object Mapper in C# with .NET Core 3.1

February 26, 2020
Written by
Gunnar Peipman
Contributor
Opinions expressed by Twilio contributors are their own

building-blazing-fast-object-mapper.png

Object-to-object mapping is used mostly for copying properties of one object to another, typically for transforming an object from one type to another. There are many use cases. For example, object transformation can help prevent overposting attacks from succeeding by intermediating between APIs and user interfaces exposed to the world and private data models on back-end servers.

You can build your own minimalistic object-to-object mapper with C# and .NET Core without writing a lot of code. This post will show you various approaches, one of which is used in production for an online action game with a massive load.

There are also professional mappers available that come with many useful features the minimalistic code  doesn’t provide. This post will show you how to implement AutoMapper, one of the most popular object-to-object mapping libraries.

The mapping techniques demonstrated here were originally developed in 2011. At the time, C# Reflection was even more costly from a performance standpoint than it is today. There was, and still is, a strong motivation to find a faster way of mapping. The approaches demonstrated in this post chart the course of a short technical journey from simple Reflection-based code to dynamic code generation in the search for better performance. As you travel the path yourself you’ll have the opportunity to see the difference in performance between the various approaches and how performance has changed over time.

 

If you would like to see a full integration of Twilio APIs in a .NET Core application then checkout this free 5-part video series. It's separate from this blog post tutorial but will give you a full run down of many APIs at once.

Understanding the case study project

In the following coding exercise you’ll create four different ways of implementing an object mapper. You’ll also implement the popular AutoMapper library, which provides a suite of highly-optimized object mapping tools. To compare the performance of each approach you’ll create a test that executes each implementation a million times and displays the elapsed time.

Prerequisites

To follow along with the coding exercises presented in this post you’ll need the following tools:

.NET Core SDK (The SDK download includes the .NET CLI and the .NET Runtime.)

Visual Studio 2019, Visual Studio Code, or an IDE or text editor of your choice

Git (If you want to clone the project from GitHub.)

.NET Core is a cross-platform technology: the code in this post will run on Windows, macOS, and Linux.

In addition to the technical resources, you should have a working knowledge of:

  • C#, including abstract classes, generic types, LINQ, and Reflection
  • .NET Core, including the LINQ and Reflection APIs
  • Creating and running .NET Core Console applications with VS 2019, VS Code, or the .NET CLI.

 It will also be helpful to have some exposure to the Roslyn code analyzers.

There is a companion repository available on GitHub.

Initializing the case study project

Create a new .NET Core Console application called ObjectToObjectMapper

Mapping the properties of two objects

The first step in object mapping is to find the matching properties of two objects. You can use the PropertyInfo class of the System.Reflection API, but you can also take a step further and be better prepared by creating a class, PropertyMap, to hold information about the matching properties.

Add a new C# class file to your project root folder named PropertyMap.cs. Replace the existing contents of the class file with the following code:

using System.Reflection;
public class PropertyMap
{
    public PropertyInfo SourceProperty { get; set; }
    public PropertyInfo TargetProperty { get; set; }
}

This class will match one property from the source object and one from the target object. To find properties to be mapped you need also some code to generate a full property map.

Add another C# class file to your project’s root folder named ObjectExtensions.cs and replace the contents with the following code which provides the GetMatchingProps() method you need to create a property map:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ObjectToObjectMapper
{
    public static class ObjectExtensions
    {
        public static IList<PropertyMap> GetMatchingProps(this object source, object target)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
            if (target == null)
            {
                throw new ArgumentNullException(nameof(target));
            }

            var sourceProperties = source.GetType().GetProperties();
            var targetProperties = target.GetType().GetProperties();

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

            return properties;
        }
    }
}

This extension method is smart: it matches only those properties that can be read in the source object and that are writable in the target object. Matched properties from both objects are wrapped in the PropertyMap class and all matching properties are returned as list of PropertyMap objects.

The ObjectExtensions class is for your reference. You won’t use it in the mapping implementations you’re going to code next, but some of its code will reappear. It’s not included in the companion repository.

Comparing different approaches to object mapping

You will create four versions of an object-to-object mapper:

  1. Unoptimized
  2. Optimized
  3. Dynamic code
  4. Lightweight Code Generator (LGC)

Each  version is more complex than the previous one.

Creating a mapping base class

To avoid repeating code you’ll need a base class for mappers.

Create a new C# class file in the root folder of your project named ObjectCopyBase.cs and replace the contents with the following code:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ObjectToObjectMapper
{
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;
    }
}
}

The GetMatchingProperties base class is similar to the GetMatchingProps method you implemented in the ObjectExtensions class. Notice that the base class also has a GetMapKey() method that generates the type map key for the mappings cache.

Implementing an unoptimized mapper

The first mapper you will create is unoptimized. It’s dumb and hurts performance badly compared to all the other versions you will write. The only purpose of the unoptimized mapper is to show you the price of using Reflection.

Add a new C# class file to your project root folder called MapperUnoptimized.cs and replace the contents with the following code:

using System;

namespace ObjectToObjectMapper
{
public class MapperUnoptimized : ObjectCopyBase
{
    public override void MapTypes(Type source, Type target)
    {
    }
 
    public override void Copy(object source, object target)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
        var propMap = GetMatchingProperties(sourceType, targetType);
           
        for (var i = 0; i < propMap.Count; i++)
        {
            var prop = propMap[i];
            var sourceValue = prop.SourceProperty.GetValue(source, null);
            prop.TargetProperty.SetValue(target, sourceValue, null);
        }
    }
}
}

Experienced developers will probably have a heart attack if they see this code is about to go live. A test of the speed of this mapper produced 0.0042 milliseconds as an average for a million runs. As you’ll see, that’s substantially slower than any of the following approaches.

Implementing an optimized mapper

You can’t go live with an unoptimized mapper; you must make it work better. What you can do is to keep the property map cached so it’s generated only once.

Add a new C# class file to your project’s root folder named MapperOptimized.cs and replace the contents with the following code:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ObjectToObjectMapper
{
public class MapperOptimized : ObjectCopyBase
{
    private readonly Dictionary<string, PropertyMap[]> _maps = new Dictionary<string, PropertyMap[]>();
 
    public override void MapTypes(Type source, Type target)
    {
        var key = GetMapKey(source, target);
        if (_maps.ContainsKey(key))
        {
            return;
        }
 
        var props = GetMatchingProperties(source, target);
        _maps.Add(key, props.ToArray());
    }
 
    public override void Copy(object source, object target)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
 
        var key = GetMapKey(sourceType, targetType);
        if (!_maps.ContainsKey(key))
        {
            MapTypes(sourceType, targetType);
        }
 
        var propMap = _maps[key];
 
        for (var i = 0; i < propMap.Length; i++)
        {
            var prop = propMap[i];
            var sourceValue = prop.SourceProperty.GetValue(source, null);
            prop.TargetProperty.SetValue(target, sourceValue, null);
        }
    }
}
}

A test run for this method produced a 0.0023 milliseconds average. It’s twice as fast as  the unoptimized version.

Implementing mapping using dynamic code

Both previous implementations make heavy use of Reflection when copying object properties. Since Reflection is slow, it is probably the only place in the mapping code that you can optimize significantly.

One way you can avoid Reflection is to generate code dynamically. Generated C# code copies property values one-by-one from one object to another. Perhaps it’s not a perfect approach, but there’s a chance to gain something in performance.

To use dynamic code you’ll need to install the Microsoft.CodeAnalysis.CSharp NuGet package. This is the .NET CLI command for the current release version as of the date of this post:

dotnet add package Microsoft.CodeAnalysis.CSharp --version 3.4.0

Add a new class file to your project root folder named MapperDynamicCode.cs and replace the contents with the following code:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;

namespace ObjectToObjectMapper
{
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;
        }
 
        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}");
 
        var syntaxTree = CSharpSyntaxTree.ParseText(builder.ToString());
 
        string assemblyName = Path.GetRandomFileName();
        var refPaths = new[] {
            typeof(System.Object).GetTypeInfo().Assembly.Location,
            typeof(Console).GetTypeInfo().Assembly.Location,
            Path.Combine(Path.GetDirectoryName(typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly.Location), "System.Runtime.dll"),
            this.GetType().GetTypeInfo().Assembly.Location
        };
 
        MetadataReference[] references = refPaths.Select(r => MetadataReference.CreateFromFile(r)).ToArray();
 
        var compilation = CSharpCompilation.Create(
            assemblyName,
            syntaxTrees: new[] { syntaxTree },
            references: references,
            options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
 
        using(var ms = new MemoryStream())
        {
            EmitResult result = compilation.Emit(ms);
            ms.Seek(0, SeekOrigin.Begin);
 
            Assembly assembly = AssemblyLoadContext.Default.LoadFromStream(ms);
            var type = assembly.GetType("Copy." + key);
            _comp.Add(key, type);
        }
    }
 
    public override void Copy(object source, object target)
    {
        var sourceType = source.GetType();
        var targetType = target.GetType();
 
        var key = GetMapKey(sourceType, targetType);
        if (!_comp.ContainsKey(key))
        {
            MapTypes(sourceType, targetType);
        }
 
        var flags = BindingFlags.Public | BindingFlags.Static | BindingFlags.InvokeMethod;
        var args = new[] { source, target };
        _comp[key].InvokeMember("CopyProps", flags, null, null, args);
    }
}
}

Compared to the previous versions it’s faster. The test run result was 0.0014 milliseconds.

Implementing object mapping using Lightweight Code Generation (LGC)

Your next experiment is to use Lightweight Code Generation. It’s an API to generate code by emitting Intermedia Language (IL) instructions directly. There’s no C#, only IL instructions, and so no C# compiler is involved.

Add a new C# class file to your project’s root folder named MapperLcg.cs and replace the contents with the following code:

using System;
using System.Collections.Generic;
using System.Reflection.Emit;

namespace ObjectToObjectMapper
{
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);
    }
}
}

With this mapper a test run gave an average result of  0.0013 milliseconds for 1 million iterations.

Comparing the performance of object mapping methodologies over time

Over the years the C# language and its compiler have improved. Also, the .NET Framework is not the same as before. Now that you’ve built object mappers with the latest version of .NET Core, it’s time to take a look back at the original experiment and compare results for the various approaches you’ve implemented.

Then, as now, the mapping techniques you’ve implemented are benchmarked against AutoMapper,  a component with a long history during which it has acquired tons of optimizations.

Implementation

Old (ms)

New (ms)

Difference (x)

MapperUnoptimized

0.0403

0.0042

9.6

MapperOptimized

0.0240

0.0023

10.4

MapperDynamicCode

0.0058

0.0014

4.1

MapperLcg

0.0019

0.0013

1.5

AutoMapper

0.0118

0.0002

59.0

These results clearly show that the current version of .NET Core is more performant. There is substantially better performance for the first two mappers, which don’t use any advanced approaches. Also notice the last line. The authors of AutoMapper have done a great job optimizing their mapper and now it seriously beats all the minimalistic approaches.

Testing object mapper methodologies

You can write a simple test of the object mappers you’ve created by using the Stopwatch class of the System.Diagnostic namespace. The Stopwatch class measures the actual time it takes code to run. To get a reliable value, every mapper is invoked one million times and the average time is reported as the result.

You can also compare the performance of the various approaches to object mapping that you’ve implemented in this project to the results from the AutoMapper library. To do that, you’ll need to add the AutoMapper NuGet package to your project. The following is the appropriate .NET CLI command:

dotnet add package AutoMapper --version 9.0.0

When the package is successfully installed, replace the contents of Program.cs with the following code:

using System;
using System.Diagnostics;
using AutoMapper;

namespace ObjectToObjectMapper
{
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);
        TestAutoMapper(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 MapperUnoptimized(),
                                new MapperOptimized(),
                                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);
        }
    }
 
    static void TestAutoMapper(OrderModel source, OrderModel target)
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<OrderModel, OrderModel>();
        });
 
        var mapper = new Mapper(config);
 
        mapper.Map(source, target);
 
        var stopper = new Stopwatch();
        var testRuns = 1000000;
 
        stopper.Start();
 
        for (var i = 0; i < testRuns; i++)
        {
            mapper.Map(source, target);
        }
 
        stopper.Stop();
 
        var time = stopper.ElapsedMilliseconds / (double)testRuns;
        Console.WriteLine("AutoMapper: " + time);
    }
}
}

Run the program. After a brief wait (depending on your hardware) you should start to see results like the following appearing in the console window opened by the program:

MapperUnoptimized: 0.004259
MapperOptimized: 0.002288
MapperDynamicCode: 0.00146
MapperLcg: 0.001453
AutoMapper: 0.000267

Press any key to exit …

If your results are typical, you should see that AutoMapper is approximately an order of magnitude faster than the other approaches. While it might be appropriate to implement your own mapper in some circumstances, you’ll need to make many optimizations to equal the performance of this popular library.

Summary

Congratulations, you just walked the path from a simple object-to-object mapper to more performant dynamic approaches! You’ve also seen how the relative speed of these techniques has changed after nine long years. Many things have changed: there are newer and better versions of operating systems, the .NET Framework has evolved into .NET Core, compilers have improved, and hardware is faster. The numbers from past tests are very different from current results.

The journey you just went through also taught you some other important things: Reflection comes with a price, but dynamic code performs very well today. You also saw how to use lightweight code generation. These approaches have stood the test of time, and they have often been the starting points of solutions for other problems.

Additional resources

There are a number of documentation references, articles, and posts that may be useful to you as you dig deeper into the topics mentioned in this post:

Introduction to the Roslyn Scripting API – This post provides some background on the technologies providing the foundation of Lightweight Code Generation.

Code Generation on .NET – A brief blog post providing an overview of the different kinds of code generation in .NET and their advantages and disadvantages.

Reflection in .NET – Canonical documentation for the System.Reflection namespace.

Gunnar Peipman is ASP.NET, Azure, and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he has been a Microsoft MVP specializing in ASP.NET. You can reach him through his website, gunnarpeipman.com.