Mastering at Source Generators

Enis Necipoğlu
C# Programming
Published in
9 min readJan 15, 2022

I’ve been working on Source Generators for a while and some of my libraries have a feature that is based on source generators. But I want to make a demonstrate about dynamic usage of source generators and show you how useful it is. In this article, I’ll guide you to create csharp files from an entity using different templates such as Controller, Domain Service, Repository. All of them will be generated with source generators from a txt template. So, all the CRUD operations will be ready at the beginning of a project.

Creating a new solution

As you know, source generators work like an analyzer and provide an analyzing source code option in development-time even code can’t be compiled. So, we need a separate project. Let’s start with creating a new webapi which is named Awesome.Api and create a .netstandard2.0 class library named Awesome.Generators.

Make sure the solution structure is like below:

I personally prefer not to use Nullable Reference Types, so I just removed <Nullable>enable</Nullable>section from .csproj files.

Development of the Syntax Receiver

Following references must be added to Awesome.Generators project:

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
</ItemGroup>

Now, we’re able to analyze code. You need to decorate your class with [Generator] attribute and implement ISourceGenerator interface to create a source generator.

using Microsoft.CodeAnalysis;namespace Awesome.Generators;[Generator]
public class ServiceGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
throw new NotImplementedException();
}
public void Initialize(GeneratorInitializationContext context)
{
throw new NotImplementedException();
}
}

You’ll see there are 2 methods to implement.

Initialize method will work at the beginning while initializing analyzer. You can register for some events or one-time configurations can be performed here.

Execute method will be triggered when syntax changed. So it means while code is being written :)

We can not generate an entire class when all syntax changed events. We need to declare something like attribute usage. Defining an attribute is much more meaningful because that can prevent running code-generation while the developer adds each symbol into the code.

Let’s create an attribute with name GenerateServiceAttribute and decorate that class is like below:

namespace Awesome.Generators;[AttributeUsage(AttributeTargets.Class)]
public class GenerateServiceAttribute : Attribute
{
public GenerateServiceAttribute(string template = null)
{
}
}

Just added a template parameter to the constructor and didn’t set to any field or property because I don’t need it at runtime. I’ll need only syntax that is written on the source code. It’s for only decoration. I’ll use the constructor parameter while generating code.

Now we can create a SyntaxReceiver to track adding a new attribute and to check if that attribute is GenerateServiceAttribute. We need to create a syntax receiver for this.

I’ll need a small extension method, add the following class to your project before creating SyntaxReceiver:

namespace Awesome.Generators;public static class StringExtensions
{
public static string EnsureEndsWith(
this string source,
string suffix)
{
if (source.EndsWith(suffix))
{
return source;
}
return source + suffix;
}
}

You’re ready to create AttributeSyntaxReceiver:

Now we can go back to ServiceGenerator class and register our the newly created AttributeSyntaxReceiver. Add registration code into Initialize method like below.

public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() =>
new AttributeSyntaxReceiver<GenerateServiceAttribute>());
}

Working with Templates

Before starting working on Execute method, let’s create a default template to use for code-generation. A simple txt file is enough. I want to use a Controller template by default to generate CRUD operations for an entity. Using a templating engine like Scriban might be very useful but you can’t use the project references at runtime because analyzers work in Roslyn process and you can only access modules that are loaded by Roslyn. Of course, there is always a way to load your dependencies on Roslyn but it’s not the topic of today :)

  • Create a Templates folder under Awesome.Generators project.
  • And mark all txt files are embedded resource in .csproj file via adding following group inside <Project> tags.
<ItemGroup>
<None Remove="Templates\**\*.txt" />
<EmbeddedResource Include="Templates\**\*.txt" />
</ItemGroup>
  • Create Default.txt under Templates folder.
using Microsoft.AspNetCore.Mvc;
using {{Namespace}};
namespace {{PrefferredNamespace}}.Controllers;[ApiController]
[Route("api/[controller]")]
public partial class {{ClassName}}Controller : ControllerBase
{
private static readonly List<{{ClassName}}> _collection = new();
[HttpGet]
public virtual List<{{ClassName}}> Get()
{
return _collection;
}
[HttpGet("{id}")]
public virtual {{ClassName}} GetSingle(Guid id)
{
return _collection.SingleOrDefault(x => x.Id == id);
}
[HttpPost]
public virtual void Create({{ClassName}} item)
{
item.Id = Guid.NewGuid();
_collection.Add(item);
}
[HttpPut("{id}")]
public virtual void Update(Guid id, {{ClassName}} item)
{
var existing = _collection.Single(x => x.Id == id);
item.Id = id;
_collection.Remove(existing);
_collection.Add(item);
}
[HttpDelete("{id}")]
public virtual void Delete(Guid id)
{
var existing = _collection.Single(x => x.Id == id);
_collection.Remove(existing);
}
}

We can’t use the scriban at the moment but I like that convention.

Let’s create a class to keep parameters strong-typed. Create a class with name DefaultTemplateParameters.

namespace Awesome.Generators.Templates;public class DefaultTemplateParameters
{
public DefaultTemplateParameters(
string typeName,
string typeNamespace,
string rootNamespace)
{
ClassName = typeName
?? throw new ArgumentNullException(nameof(typeName));
Namespace = typeNamespace
?? throw new ArgumentNullException(nameof(typeNamespace));
PrefferredNamespace = rootNamespace
?? throw new ArgumentNullException(nameof(rootNamespace));
}
public string ClassName { get; set; } public string Namespace { get; set; } public string PrefferredNamespace { get; set; }
}

Development of Source Generators

Now we can go back to our Execute method in ServiceGenerator.

The entire content of Generator:

I’ve written some comments inside the code. It should explain itself. If not you can contact me and we can discuss later. I’ll be delighted if you do :)

Executing the Source Generator

We’ve done with Awesome.Generators project. Let’s make it running.

Add Awesome.Generators reference to the Awesome.Api project. But be careful. It’s not a standard project reference. We must mark that reference as Analyzer via using OutputItemType parameter.

<ItemGroup>
<ProjectReference
Include="..\Awesome.Generators\Awesome.Generators.csproj"
OutputItemType="Analyzer" />
</ItemGroup>

Now you can see our project as an analyzer in Visual Studio.

There is no content yet because we haven’t used GenerateServiceAttribute and generated any of code. Let’s start with creating a new entity with our attribute which is named Todo.

using Awesome.Generators;namespace Awesome.Api;[GenerateService]
public class Todo
{
public Guid Id { get; set; }
public string Content { get; set; }
public bool IsCompleted { get; set; }
}

The project should be built once after referencing a new analyzer. After the build was completed, you’ll see the generated code under analyzer in solution explorer.

If the generated codes weren’t showed up, your IDE requires to be restarted. But it’s not necessary, you can still run the application.

Now hit the ▶️Run button. You’ll see the CRUD endpoints which come from the controller generated by source generators.

Now you can create new entities as much as you desire with CRUD endpoints.

Accessing to Database with a Custom Template

You can inject a repository to controllers via editing code template and make your controller use database to perform CRUD operations.

I assume the ID is Guid by default.

I’ll use an interface to define Ids on entities which is named as IIdentifiable, because I’ll make operations over Id.

public interface IIdentifiable
{
Guid Id { get; set; }
}

Don’t forget to implement it to our first entity named Todo.

[GenerateService]
public class Todo : IIdentifiable
{
public Guid Id { get; set; }
public string Content { get; set; } public bool IsCompleted { get; set; }
}

Before creating a custom template we need an abstract repository. Let’s call that as IRepository. Create a simple repository interface. We can add it to the main app (Awesome.Api).

I’ll use MongoDB implementation to get rid of DbContext implementation & migrations. Add Mongo.Driver and Humanizer.Core reference to your project. I used Humanizer for just pluralizing collection names. You can pass it if you don’t need it.

<PackageReference Include="MongoDB.Driver" Version="2.14.1" />
<PackageReference Include="Humanizer.Core" Version="2.13.14" />

Add a MongoDbOptions class.

using System.Reflection;namespace Awesome.Api.Data;public class MongoDbOptions
{
public string ConnectionString { get; set; } = "mongodb://localhost:27017/" + Assembly.GetEntryAssembly().GetName().Name.Replace(".", "_");
}

And the implementation of MongoDbRepository.

Don’t forget to register it into ServiceCollection in Program.cs

builder.Services.Add(
new ServiceDescriptor(
typeof(IRepository<>),
typeof(MongoDbRepository<>),
ServiceLifetime.Transient));

Now, the repository pattern implementation is ready to use. We need to create another controller template that uses IRepository. Create a new file with the name RepositoryController.txt under Templates folder in Awesome.Api project and mark it as C# analyzer additional file. After making this, this file won’t be included in your assembly or won’t be copied to the output directory. This file only is accessed by analyzers.

And the content of file is below.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Awesome.Api.Data;
using {{Namespace}};
namespace {{PrefferredNamespace}}.Controllers;[ApiController]
[Route("api/[controller]")]
public partial class {{ClassName}}Controller : ControllerBase
{
private readonly IRepository<{{ClassName}}> _repository;
public {{ClassName}}Controller(
IRepository<{{ClassName}}> repository)
{
_repository = repository;
}

[HttpGet]
public virtual Task<List<{{ClassName}}>> GetListAsync()
{
return _repository.GetListAsync();
}

[HttpGet("{id}")]
public virtual Task<{{ClassName}}> GetSingleAsync(Guid id)
{
return _repository.GetSingleAsync(id);
}
[HttpPost]
public virtual async Task CreateAsync({{ClassName}} item)
{
await _repository.InsertAsync(item);
}
[HttpPut("{id}")]
public virtual async Task UpdateAsync(Guid id, {{ClassName}} item)
{
await _repository.UpdateAsync(id, item);
}
[HttpDelete("{id}")]
public virtual async Task DeleteAsync(Guid id)
{
await _repository.DeleteAsync(id);
}
}

Only placing template parameter to attribute is left. Go to our first entity named Todo and add the RepositoryController.txt parameter to GenerateServiceAttribute.

[GenerateService("RepositoryController.txt")]
public class Todo : IIdentifiable
{
public Guid Id { get; set; }
public string Content { get; set; }
public bool IsCompleted { get; set; }
}

Now, re-build your project and see the generated file under the analyzers section in solution explorer is changed. If you have a running MongoDB instance, you’ll be able to perform all CRUD operations on MongoDB.

If you don’t have MongoDB instance but have already installed docker, you can execute this command to run a mongodb instance without pain.

Just create, update and delete a couple of todo items and see it’s working.

Extending the Generated Class

You can use [GenerateService] attribute for your all entities and all entities will have the same pre-generated controllers, services, repositories, etc.

But sometimes, you need to customize generated classes. You have 2 ways to do that: Using a Partial Class and Overriding via Inheriting.

Using a Partial Class

Generated codes are included in your project, assembly. So, they don’t come from yet another assembly. That means you can create partial classes to make some additions to them.

Let’s talk about adding a new endpoint that marks all as completed.

Create a physical C# class named as TodoController.cs in the same namespace as generated one. In that case, we’re creating under the Controller folder.

The compiler says you can’t have TodoController class in this namespace, because you already have it. If you see that message, you’ve created it in the correct namespace. Just make that class partial, the error will be disappeared.

Now we’re ready to add a new endpoint for TodoController.

Run the application and see the newly created endpoint by you.

Overriding via Inheriting

When you create a new Controller and it inherits from TodoController, you’ll have 2 real controllers. You should make the base TodoController abstract via writing a tiny partial class to prevent pretending the base class as a controller.

Conclusion

You can generate infinite Controllers, Services, Repositories, etc. that includes CRUD operations for your all entities. As a next step, you can add generate business services, and use that generated business services in generated controllers. Even you can add mapping and caching to your generated files to make them more suitable for enterprise solutions. The sky is the limit!

I would be glad if this example sheds light on the use of Source Generators.

Source Code

Also source code of this example is available on GitHub:

--

--