Source Code Generators, DIY

Making your own Source Code Generator is only a few command lines away :-)

Bnaya Eshet
Stackademic

--

In a previous post, we explored the wonders of source generators in .NET, shedding light on how they can boost your development efficiency. Today, we’re taking it a step further. Instead of relying on existing generators, we’ll delve into the exciting world of creating your very own custom source generator. This opens up a world of possibilities, allowing you to address specific problems or domains with tailored solutions. In this article, we’ll walk you through the process of creating a custom source generator that mimics Refit typed HTTP client generation, showcasing the incredible potential of custom generators.

Recap: The Power of Source Generators

Before we dive into the nitty-gritty of custom source generators, let’s briefly recap why source generators are such a game-changer.

Source generators enable automatic code generation during compilation, saving you time and effort by eliminating repetitive tasks. Whether it’s creating boilerplate code, serialization logic, or handling complex data structures, source generators can handle it all. In a prior article, we uncovered the magic of leveraging pre-built generators, such as the mapping object, builder pattern generator, and more, to bid adieu to boilerplate code. Now, let’s empower ourselves to craft our own.

Two commands away

To harness the power of source generators and have a full working solution, already configured to the task, use the following command lines.

  • Iinstalling the .NET template.
dotnet new install Bnaya.SourceGenerator.Template
  • Scaffold a fully configured solution, with a source generator sample and debug configuration in place.
dotnet new srcgen -n JediForce

Taking a close look

You have crafted your first fully functional Source Code Generator.
Take a look at your creation, the solution parts are:

  • JediForce project: this is where you generate the code
  • JediForce.Abstractions: An abstraction project is where you define non-genative code, it could be an attribute that will mark a code section for being the base for the generator.
  • Playground (folder): This is where you can check it.

A Sample

The template scaffolding sets up a sample that takes an interface and generate a typed http proxy for a service.

How do you generate a source code?

[Generator]
public partial class Generator : IIncrementalGenerator
{
private static bool AttributePredicate(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
return syntaxNode.MatchAttribute(TargetAttribute, cancellationToken);
}

public void Initialize(IncrementalGeneratorInitializationContext context)
{
// The Magic
}
}

Initialize responsible for:

  • Identify the code section the generator takes as its input.
  • Transform it into a convenient structure.
  • Generate a new source code (upon the input).
private static bool AttributePredicate(SyntaxNode syntaxNode, CancellationToken cancellationToken)
{
// predicate for the code section (in this case looks for an attribute)
return syntaxNode.MatchAttribute("ProxyAttribute", cancellationToken);
}

public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<SyntaxAndSymbol> classDeclarations =
context.SyntaxProvider
.CreateSyntaxProvider(
predicate: AttributePredicate, // identify the code section
transform: static (ctx, _) => ToGenerationInput(ctx))
.Where(static m => m is not null);

IncrementalValueProvider<(Compilation, ImmutableArray<SyntaxAndSymbol>)> compilationAndClasses
= context.CompilationProvider.Combine(classDeclarations.Collect());

// register a code generator for the triggers
context.RegisterSourceOutput(compilationAndClasses, Generate);

static SyntaxAndSymbol ToGenerationInput(GeneratorSyntaxContext context)
{
// pack the code section reference into a convenient structure
var declarationSyntax = (TypeDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(declarationSyntax);
if (symbol is not INamedTypeSymbol namedSymbol) throw new NullReferenceException($"Code generated symbol of {nameof(declarationSyntax)} is missing");
return new SyntaxAndSymbol(declarationSyntax, namedSymbol);
}

void Generate(
SourceProductionContext spc,
(Compilation compilation,
ImmutableArray<SyntaxAndSymbol> items) source)
{ // Invoke the actual code generation
var (compilation, items) = source;
foreach (SyntaxAndSymbol item in items)
{
OnGenerate(spc, compilation, item);
}
}
}
private void OnGenerate(
SourceProductionContext context,
Compilation compilation,
SyntaxAndSymbol input)
{
INamedTypeSymbol typeSymbol = input.Symbol;
TypeDeclarationSyntax syntax = input.Syntax;
var cancellationToken = context.CancellationToken;
if (cancellationToken.IsCancellationRequested)
return;

// This is your part, Generate whatever you 💙 (run the command line and check it out)
}

You can try it and take a look at how the sample generates a typed HTTP proxy based on an interface decorated with attributes, it’s very much like Refit does, yet unlike Refit it does it with code generation.

When taking a look at the Playground folder you can see:

[Proxy("calc")]
public interface ICalcProxy
{
[ProxyRoute]
Task<int> GetAsync();

[ProxyRoute("add", Verb = HttpVerb.POST)]
Task<int> AppendAsync(Payload payload);

[ProxyRoute("subtract", Verb = HttpVerb.POST)]
Task<int> SubtractAsync(Payload payload);
}

It will result in the generation of two classes:

[System.CodeDom.Compiler.GeneratedCode("JediForce","1.0.0.0")]
internal class CalcProxy: ICalcProxy
{
private readonly HttpClient _httpClient;

public CalcProxy(HttpClient httpClient)
{
_httpClient = httpClient;
}

async Task<int> ICalcProxy.GetAsync()
{
var result = await _httpClient.GetFromJsonAsync<Int32>("calc");
return result;
}

async Task<int> ICalcProxy.AppendAsync(Payload payload)
{
var content = JsonContent.Create(payload);
var response = await _httpClient.PostAsync("calc/add", content);
if(!response.IsSuccessStatusCode)
throw new HttpRequestException("calc/add failed", null, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<Int32>();
return result;
}

async Task<int> ICalcProxy.SubtractAsync(Payload payload)
{
var content = JsonContent.Create(payload);
var response = await _httpClient.PostAsync("calc/subtract", content);
if(!response.IsSuccessStatusCode)
throw new HttpRequestException("calc/subtract failed", null, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<Int32>();
return result;
}
}

And

[System.CodeDom.Compiler.GeneratedCode("JediForce","1.0.0.0")]
public static class CalcProxyDiExtensions
{
public static IServiceCollection AddCalcProxyClient(this IServiceCollection services, Uri baseUrl)
{
services.AddHttpClient<ICalcProxy, CalcProxy>("calc", client => client.BaseAddress = baseUrl);
return services;
}
}

Debugging

Debugging is a vital aspect of success, and having a solid debugging environment is key. Thankfully, the solution is well-prepared for debugging, offering a great debugging experience.

To debug source generation, make sure to set up the source generation project as the startup project and establish a breakpoint.

Behind the scene

A few essential techniques are required to set up a source generator project:

  • The project reference to the source generator should be structured as follows:
<ItemGroup>
<ProjectReference Include="..\JediForce\JediForce.csproj"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />
</ItemGroup>
  • Setting up the debugging experience (under project properties):
  • If you like to add a reference to another project, make sure to select Roslyn Component.

CI/CD

As part of the template, you get a full working CI/CD which will deploy it as a NuGet (If you host it on GitHub).
Check the GitHub Workflow under the .github folder (GitHub Actions on the solution explorer).

You still have to configure a few settings on nuget.org and on your repo:

  • Set this Key into GitHub Setting secrets.
    Add `NUGET_PUBLISH` key under /settings/secrets/actions.
    and Allow Read and write permission (because the workflow increments the version on each deployment)

Conclusion

In sum, code generation is a practical reality in software development. It improves code quality, keeps things consistent, reduces errors, and cuts out repetitive work. By automating these tasks, it frees up developers for more creative problem-solving. Embracing code generation isn’t just a step forward; it’s a leap into a more efficient and excellent future of software development.

Tip

If you are not sure what the right Roslyn syntax use ChatGPT it’s doing a decent job.

Resources:

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--