March 14, 2017 // By Jason Bock
Synposis: A common technique in .NET code is to map the state between two objects. Typically, this happens at runtime with packages that automatically generate mapping strategies. However, what if you could automatically generate that code before you even compile it? In this article, I’ll look at what you can do with the Compiler API to generate mapping code and what limitations this technique has.
Source, Meet Destination
Automatic object mapping. You’ve probably seen this in projects you’re working on, or maybe you’ve heard of Nuget libraries that handle this for you. Mapping is a simple operation to perform: you take the state of one object and set it to the state of another. This is done by finding all the public properties from the source object that are readable and matches them to public properties on the destination object that are writable, have a compatible type, and have the same name as the property from the source. Here’s an example. Let’s define two classes,
To map an instance of
Destination, you write this code:
Easy, right? For a simple example like this, it is. However, real-world coding scenarios are never as easy as this. Consider these cases:
- What if your source has properties that can’t be mapped to a destination property because there’s no matching property by name, or the types are incompatible?
- What if you have a match between the source and the destination but you don’t want to map the property.
Now, if you were always writing your mapping code by hand, you can handle these cases as needed. However, to be frank, writing mapping code is boring and tedious. Boring and tedious code is also prone to errors – it would be easy for a developer to forget a property to map, especially if the objects are large with numerous properties. This is why you’ll find packages like AutoMapper on NuGet. It simplifies the mapping process down to just a couple of lines of code:
If you wanted to ignore a specific property, you specify that during configuration:
There’s a lot more you can do with AutoMapper, and I encourage you to read the documentation on what is possible with this package.
Note: I highly encourage you to read this article on object mapping, as it covers bad practices that you should avoid and provides guidance on when to use packages to handle mapping automatically.
Issues with Automatic Mapping
While AutoMapper is a great package that can simplify mapping concerns, it also has some drawbacks. One of the main issues is performance. An automatic mapper has to use Reflection to find all the properties on the source and destination and determine what properties should map. Some of this work, once discovered, can be compiled via Reflection.Emit or LINQ Expressions and subsequentially cached, delivering better performance. However, this discovery process is still a one-time hit that you can’t avoid.
There are other mapping issues that have to be addressed, and to the author of AutoMapper’s credit, a lot of work has been done to reduce the performance penalty of run-time mapping. Furthermore, in most applications, an automatic mapper like AutoMapper will not be the source of major performance issues. However, there’s no beating compile-time mapping. Even a library like TinyMapper, which claims that its mapping performance is two orders of magnitude faster than AutoMapper, can’t beat handwritten mapping code.
Personally, I’ve found that automatic mappers can sometimes be more trouble than they’re worth, especially as the object graph becomes complex. More often than not, I end up manually writing the mapping code myself. That’s still a tedious process, and I’d love it if a tool would generate that mapping code for me. That’s what I’m going to demonstrate in this article – using the Compiler API to generate mapping code directly in my source code. It won’t be a full-fledged mapper; rather, I’m focusing on the process of finding these mapping points and transforming source code to including mapping code.
Creating Mapping Code at Coding Time
So, how do you do this? The approach I took was to create an analyzer that will look for a specific method invocation, and will provide a code fix to remove that method invocation and replace it with mapping code. This means that if you want to follow the code, you'll need to know how the Compiler API works. This isn't an easy endeavor to do, but there's a lot of information online to get you up to speed if you've never used this API before.
To start, here’s the method that the analyzer will look for:
Invoking this method will cause an exception. The intent of this method is to be a “marker” in code. That is, the analyzer will look for this method and flag any usage as an error, forcing the developer to address it. The following figure shows what this looks like in Visual Studio:
Figure 1: Flagging a Map() invocation as an error
To get the method invocation to show up as an error, I wrote a custom analyzer called
UsingInlineMapperAnalyzer. This analyzer looks for any method invocation, and if that method invocation is the
Map() method on
InlineMapper, it reports it as a problem. Here’s the analyzer code:
Initialize(), I tell the context to call
AnalyzeInvocationExpression() whenever an
InvocationExpressionSyntax node is found within a C# syntax tree. Then, I look at that node, and determine if it’s the method I’m looking for. Note that I’m using a
SemanticModel to get a
IMethodSymbol reference for the
InvocationExpressionSyntax node I was given. Symbols from the semantic model are a little easier to work with than the syntax nodes, tokens and trivia that you get from the syntax tree. Getting the method name and its containing type and assembly is straightforward with
That’s one part of the solution. The other part is to replace the method invocation with object mapping code. This is done with a code fix class called
UsingInlineMapperCodeFix, which inherits from
CodeFixProvider. This takes a bit more code than the analyzer, and I’ll only focus on the
RegisterCodeFixesAsync() method I overrode to create the new code:
That’s a lot of code. Let’s cover each step in detail. The first 10 to 20 lines of code get the root and model for the document that contains the
InvocationExpressionSyntax node found in the analyzer. Once we grab the
InvocationExpressionSyntax object, we get the method and type arguments for the method. This enables us to figure out the names of the properties and their types. For example, if the destination object has a property defined like this:
We’ll know that its name is “Id” and its type is “int”.
Next, we find all the properties on the source object that are readable. We try to find a property on the destination object that has the same name, is writeable, and has a compatible type. If we find one, we generate a
StatementSyntax object that would look something like this:
If the type of the source property isn’t assignable to the destination property’s type, we generate a
SyntaxTrivia node that contains a
TODO comment. This will be a tip to the developer that something isn’t quite right between the source and destination objects.
The last step is to create a new tree if we have either expressions or comments. This is done in
If all I have is new expressions, it’s pretty easy – I use
ReplaceNode() to remove the original invocation with my new property mapping expressions. Adding comments throws a wrinkle into the equation.
SyntaxTrivia objects, and they are added as either leading or trailing trivia to a node in the tree. Therefore, I have to add the comments to different nodes if I actually have expressions or not.
So, let’s see this in action. I open a solution that has the analyzer and code fix enabled with two classes defined as my source and destination:
This is what I see with the code fix:
Figure 2: Code Fix offering for mapping
Keep in mind that I can apply the code fix for any instance in my solution where my analyzer has found code where I’m calling
InlineMapper.Map(). If I accept the code fix, my code changes like this:
Figure 3: Mapping code applied
Notice that I now have a comment that I can track in Visual Studio via the Task List:
Figure 4: Mapping issues in the Task List
Now I have my mapping code inline and have the best possible performance for object mapping. Perfect!
Issues with Compile-Time Mapping Code
Now, my example doesn’t have all the features you’d want for object mapping. For example, I don’t handle child objects at all. My intent with this example wasn’t to provide a fully functional inline object mapper. If you want to extend this idea for any features you want, feel free to take this code and adjust it for you needs. However, keep in mind that it has one problematic issue. Generating mapping code happens once. Run-time mappers like AutoMapper have the advantage that they’ll always work with the latest versions of the source and destination objects. My approach will completely ignore new versions of the source and destination types. That is, if they both have three new properties, they are not added to the code. I’d have to remember to delete the mapping code, make a call to
InlineMapper.Map() and run the code fix again.
What we really want here is a mechanism in C# that allows us to add code at compile time, every time compilation occurs. Some languages have macros that provide this capability. Currently C# doesn’t have that, but there is a proposal called source generators or code generator extensions. The name is somewhat up in the air right now – visit these pages for more information. If you think having this powerful feature would be beneficial for C#, add your enthusiasm to these discussions!
In this article, I talked about object mappers in .NET. I discussed how they work and what some of their limitations are. I demonstrated an alternative approach using the Compiler API. Finally, I mentioned C# language proposal that could make code generation a first-class citizen with the language. I hope that you start looking at the Compiler API and use it to enhance and empower you C# development. Until next time, happy coding!