This will be a very simple post about how we can use C# source generators to map minimal API endpoints automagically.
The obvious way to do this automatic endpoint mapping, would be to do reflection based assembly scanning, which is a very common approach to do these kinds of things, like registering services, which, for example, we can do pretty easily with libraries like Scrutor.
Although reflection based assembly scanning works, there’s just a little something we can do better: performance. Reflection isn’t the fastest thing, so if we can avoid it and just have things put in place at compile time, there’s one less thing slowing down our application’s startup. Additionally, .NET Native AOT and reflection might not go too well together (still early days though), so it’s good to already start looking at possibilities.
I’m pretty late to the C# source generator party, but hey, better late than never 😅. Not only am I pretty late, but I’m pretty sure what I’m talking about in this post, has already been talked loads of times, but I wanted to try things out for myself, so, like many other of my posts, if for no one else, it’ll be relevant for future me 🙃.
One final note on the post, is that the code you’ll was just to try things out, so things should be improved for actual production use (e.g. better type safety and avoiding finding things with hardcoded strings). But that’s hopefully something you already assume when looking at code in blogs 🙂.
The API and hooking points
Let’s start with the API (which is as basic as it can be), as well as, more importantly, the integration points put in place for the source generator to hook into.
For starters, we have an
IEndpoint interface, exposing a single
static abstract method to implement (new feature in C# 11). This will allow the source generator to look for all implementations of the interface.
Then, we have the most basic hello world implementation:
The whole point of this post, is that we don’t want to call
HelloEndpoints.Map manually, so instead, in the
Program.cs, we call a method
RegisterEndpoints, which will be implemented by the source generator, to map all endpoints:
RegisterEndpoints method is defined as a partial extension method in a partial
The importance of this class and method being
partial, is that then the source generator can generate code for the same class, actually implementing the method. Source generators cannot modify existing code, only add more code, so this is a way to leave our type open for extensibility, allowing our source generator to contribute to it.
Bootstrap the source generator
I’m just going to quickly skim past this one, as this is one of the first things in the docs: we need to create a .NET Standard 2.0 class library, with references to the
Generators.csproj looks like the following:
There’s some extra stuff I didn’t mention (like nullable and language version bits), but that’s just about some of my preferences, not really source generator related.
With the project in place, we can reference it from the API project, like so:
Then we can create our source generator class. This class should be decorated with the
Generator attribute, and inherit from
ISourceGenerator, which will provide us a couple of methods to implement.
Collecting required information
Before generating the endpoint mapping code, the source generator needs to do two things: find the
EndpointRegistrationExtensions class we want to extend, as well as find all implementations of
IEndpoint, so we can map them.
There are a couple of ways to do this: in the execute method, using the
context parameter go through the all nodes and find what we want; implement an
ISyntaxReceiver that we configure to be invoked by the runtime for each node it finds. I’m not sure I have a preference between them at this point, so for no particular reason, I went with the latter. If you want to learn more, Khalid wrote a post on the subject.
ISyntaxReceiver implementation, which I named
Collector and is an internal class of the source generator, looks like this:
As you can see, it’s not something particularly esoteric. We check if the node in the context is a class, then check if it’s one of the two things we’re looking for: the partial class we’ll extend, or an implementation of
IEndpoint (yes, doing hardcoded string comparison isn’t a good idea, I warned you at the beginning of the post 😜). We then expose this information in properties for the source generator to use.
To configure the
Collector to be used, we implement the source generator
Initialize method like so:
Generating the code
With the bulk of the work done, all that’s left is to generate the code. We do this in the source generator
Execute method, which looks like the following:
Let’s go step by step, they should be mostly self explanatory from the code.
We start by casting the context’s
SyntaxtContextReceiver property to our
Collector, so we can access our collected data.
Then, we grab the namespace in which the
EndpointRegistrationExtensions lives, so we add the partial to the same one.
We then compose the lines of code invoking the
Map method on all
IEndpoint implementations we found.
With these things ready, we compose the final code, which is just a plain string. The code is again using some recent C# features, namely raw string literals, to make it much more readable (plus that
// lang=C# comment, is a JetBrains Rider feature that enables syntax highlighting within the string 🤯). This syntax highlighting is a bit messed up in the blog, as my blog engine doesn’t understand C# 11 🙃.
Finally, we invoke
context.AddSource to add our code to the solution.
With all of this in place (assuming I didn’t forget any step while writing this post), we can now run our API and everything will work as we hoped 🙂.
That’s it for this quick look at how we can implement automatic discovery and mapping of minimal API endpoints with C# source generators.
As I mentioned, this was a quick and dirty test of how things could be done, so the code would need some extra effort to be a bit more production ready.
In any case, it was a good exercise to have a better understanding of how source generators work and how we can use them to solve some common problems.
- Source code repository
- Source Generators - Microsoft Docs
- .NET Source Generators: Finding Class Declarations
- What’s new in C# 11
Thanks for stopping by, cyaz! 👋