DEV Community

Cover image for Using C# Source Generators to Generate Data Transfer Objects (DTOs)
Aman Agrawal for Coolblue

Posted on • Updated on • Originally published at amanagrawal.blog

Using C# Source Generators to Generate Data Transfer Objects (DTOs)

For many enterprise applications, there would normally be a split between domain entities that live inside the application core and DTOs that are exposed to the outside world, for e.g. as outgoing data structures from a web service. Often these structures are symmetric to domain entities, i.e. they contain all of the same properties that domain entities contain but most likely none of the logic. There are 2 problems with this pattern however:

  • This tends to be a very manual and time consuming task that doesn't add much value
  • The code you have to write to map from the domain entities to these DTOs tends to be repetitive and error prone. Mistakes can often occur leading to broken contracts with the consumers of the DTO.

Using C# Source Generators to generate DTOs could potentially save a lot of developer time, so in this post I am going to attempt to write just such a generator.

DISCLAIMERS:

  • This may not be the most performant way or the most sensible to write source generators so if you know of a better way, please by all means comment away!
  • The code shown here will very likely be very hard to follow at times especially in the last half of part 2, its OK if you don't follow all of it. You are writing code within code essentially, so it is a bit messy by design. I will put the fully refactored code on Github with a readme that describes some of the Roslyn APIs so that should help a little.
  • The whole thing is a bit of an experiment, I've not used this code in any production application yet. I intend to at some point to get some real feedback on its viability but nothing as yet. For now I am really curious to see what's possible and how far I am willing to and/or comfortable, pushing it.
  • Implementation is a bit opinionated so it will not cater for all edge cases.

ASSUMPTIONS:

  • No inheritance relationship between domain entity classes. All entity classes are therefore assumed to be at the same level of hierarchy.
  • Either the whole entity class will be mapped to a DTO or not at all, excluding properties from being mapped whilst possible is not in the current scope.
  • DTOs are assumed to be outgoing from, say a web service or a REST API so persistence related models are out of scope for now because they might require additional data access related code to be added to the DTOs for e.g. special attributes specific to ORMs, access modifiers etc which for a generic code generator is too much responsibility and will make things too complicated.

What is source generation?

At its most basic, source generation is basically what it sounds like: auto-generated code. There are two main types: compile time and post-compilation or IL emitting. Former ties into the existing build toolchain and the latter is a bit more challenging because you need to know IL and emitting correct IL is hard if not impossible. I could imagine both being relatively difficult to test using conventional testing techniques. Usually a lot of trial and error might be involved and there might often be limited support for debugging. Source generation is not a new concept, its been around for a long time in the form of tools like T4, Postsharp, Fody etc but I have never used these tools in the past...well...except for may be T4 several years ago...once.

C# Source Generators allow emitting C# code during the compilation process and include the emitted code in the rest of build process such that it builds along with the rest of your code. The compiler passes the control to the ISourceGenerator implementation to add code to the syntax tree and the emitted code is then included in the rest of compilation process as normal. This blog post goes into a lot of the "whats" and the "whys" so I am not going to.

Compiler 101

The C# compiler creates two models from the code you write: syntactic model i.e. how is the code structured in terms of tokens for e.g. starts with access modifier, then return type then identifier then parenthesis etc basically your cs file and semantic model i.e. what does the code mean for e.g. what is a property? what are the type arguments for a generic type? etc. During the compilation process, the parser parses the code into a syntax tree and generates the semantic model for this tree, these two models are then passed to your source generator to scan, and emit code based on criteria you define. For e.g. generate DTOs for all entity classes decorated with a certain attribute.

Generating DTOs from Basic Domain Entities (only primitive types)

First things first, I will add a console app (call it ConsoleApp9) to a new solution where I can define my domain entities, and later will reference the source generator to do some code generation.

For this blog post I will create a basic Employee domain entity that looks like this:

The corresponding DTOs might look something like this - basically just open property bags:

How do I get from the domain to DTO ?

The next thing I've got to do is create a new .NET Standard 2.0 class library project in my solution that I created earlier. This library project can then be shipped as a Nuget package later. NOTE: I do need all those Microsoft.CodeAnalysis.* packages to create a source generator. The C# language version has to be latest and in order to see what files the compiler outputs, I'll set the EmitCompilerGeneratedFiles attribute to true and specify a folder for the generated files to go into (last two lines in the following csproj snippet).

Now I need to create an implementation of the ISourceGenerator interface in this project and decorate it with Generator attribute for the compilation process to pick it up as a source generator.

What I want to do in order to help the source generator find the types that need converting to DTO, is decorate my domain types with a custom attribute which I will create in the source generator project:

and then tack it on the my domain entities that I want DTOs for:

In order to tell my source generator which classes to generate code for, I need to also implement ISyntaxContextReciever and register it with the source generator (in the Initialise() method). This receiver is kind of a hook that the compiler calls into as it traverses the syntax tree node by node:

During each visit, I will check to see if the TypeDeclarationSyntax node (for e.g. a class or a struct) is decorated with the GenerateMappedDto attribute and if it is, I will simply add it to a list. That done its time to register this receiver with the source generator:

TypeDeclarationSyntax is the base type for the ClassDeclarationSyntax and StructDeclarationSyntax so will cover both container types and will allow generating DTOs for both. Once the entire syntax tree is thus visited, the control will be transferred back to the source generator and the compiler will pass it both the syntactic model and the semantic model using which I can write out the actual code:

There is a quite bit going on here so I will unpack, I am using the semantic model for the most part because that gives me richer set of information about the code:

  1. I'd also like, for simplicity reasons, to put the DTOs in "Dtos" namespace under the main domain namespace. So if the entity is in MyApp.Domain then the dtos will be in MyApp.Domain.Dtos (Line 17)
  2. I will like the DTOs to have a naming convention of "{Entity class name}Dto". For e.g. Employe entity class will have a DTO class named EmployeeDto. (Line 20)
  3. I am importing some generic framework libraries. Nothing fancy about that (Lines 23-25)
  4. Then goes the meat of the class body (Lines 32-41) (more on this in a sec!)
  5. Close class and namespace definitions.

So what's the meat here?

Essentially, each property in the DTO will be mapped to the corresponding property in the domain entity, with the same name and type. So I am looping over all the PropertyDeclarationSyntax nodes in the domain entity syntax model and emitting corresponding properties. In the BuildDtoProperty() method, I am once again using the semantic model to get more information about the property type which is available via property.Type property. Then I've just added a simple extension method to get the condensed name of the type for e.g. Guid instead of System.Guid (just personal readability preference).

It literally emits: public Guid Id {get;set;}

BTW, all these various syntax/symbol classes are a part of the Roslyn C# syntax API that you can browse here.

That...is basically it for a very minimal (read: limitedly useful) DTO generator!

How do I actually use this generator in my application?

Remember our ConsoleApp9 that we added earlier? I will now add to it a project reference to the source generator project:

Two things of note here:

1) I have added the GenerateMappedDtoAttribute class as a linked file temporarily, eventually this would be a part of the Nuget package so the linked file reference can be removed from the ConsoleApp9.csproj, and

2) the project reference doesn't reference the output assembly and also sets the item type to Analyzer. The former will make sure that any transitive dependencies of the source generator project don't get added as references of the console app project itself (this attribute is not needed when adding a PackageReference) and the latter will make it appear as an analyzer under dependencies and this is also where I can see the generated DTO.

Now I am all set to generate my very first DTO automagically! I'll just hit Ctrl+Shift+B in Visual Studio (or do dotnet build from the CLI), to build the solution! If you are doing this for the first time you might notice that nothing seems to have changed in ConsoleApp9! 🤔No DTOs in sight, nothing and if you are unlucky there might be some build errors to boot as well, what's that about?

Well, this is where we might want to pay heed to the recommendation of source generator creators:

You will need to restart Visual Studio after building the source generator to make errors go away and IntelliSense appear for generated source code. After you do that, things will work. Currently, Visual Studio integration is very early on. This current behavior will change in the future so that you don’t need to restart Visual Studio.

- Microsoft

I don't think VS Code suffers from this issue and I haven't tried this on Rider, unfortunately, my trial expired before I embarked on source generators so that mystery will stay a mystery for now!

Once I do the proverbial "turn it off and on again", I now see the DTO light up in the consuming project and intellisense should also pick it up. Now bear in mind, these generated DTOs don't get checked into source control because..well..they get generated at build time!😁But they will get packaged into my application binaries during deployment, so I can rest easy!

How do I Nuget-ify my source generators?

For this I will modify the csproj of the source generator to:

  1. Add a package version (come on, we're not animals!)
  2. Instruct the dotnet pack command to put the analyser in a pre-designated folder in the generated nuget package (I ended up spending several frustrating minutes trying to figure out why the analyser was not showing up in my consuming project, this turned out to the missing piece! Now you know as well!)

Once this package is pushed to nuget feed, I can replace ProjectReference with PackageReference in my ConsoleApp9 project, remove the ReferenceOutputAssembly attribute from it and I'm off to the races! Now whenever I add a new property to my domain entity, all I have to do now is build the project and the DTO will be automatically updated. That's a lot of developer time saved potentially!

What's missing?

As cool as this was, the generator is very basic in that it doesn't support:

  • Properties with complex types for e.g. Employee class having a Address type property which in turn could be composed of primitive types.
  • Properties with generic types with one or more primitive type arguments for e.g. Employee class having an IReadOnlyCollection<string> property called Dogs (may be for some godforsaken reason we want to track the names of their dogs! I know you pooped on the carpet, Winston! 🤷‍♂️)
  • Properties with generic types with one or more complex type arguments for e.g. Employee class having a IReadOnlyCollection<CompanyAsset> called AssetsAllocated or some completely made up property of type Dictionary<int,Spaceship>, and
  • Generating mapping extension methods to convert entities to DTOs. This could potentially be a bigger win in terms of time saving!

These I will cover in the final part of this blog post!

Header image source

Checkout part 2 here

Top comments (0)