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.
- Using LINQ and reflection to find matching properties of objects
- Performance: Using dynamic code to copy property values of two objects
- Performance: Using LCG to copy property values of two objects
- Writing object to object mapper: first implementations
- Writing object to object mapper: moving to generics
- Writing object to object mapper: my mapper vs AutoMapper
- My object to object mapper source released
- Using Roslyn to build object to object mapper
Dynamic code in current era
As I’m new to compiler services I found some great starting materials:
- Simple sample of using Roslyn on .NET Core by Joel Martinez
- Compiling C# Code Into Memory and Executing It with Roslyn by Tugberk Ugurlu
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.
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.
Implementation | Old (ms) | New (ms) |
---|---|---|
LCG | 0.0019 | 0.0013 |
Dynamic code | 0.0058 | 0.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.
Nice article Gunnar. How does this mapper compare to the current AutoMapper?
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.
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.