Writing simple C# source code generator with Fluent API

Introduction

Usually in .NET world when you want to write generic code you use generic method or classes. But sometimes it is still not enough when you want to automate some things. I.e. you want to dynamically register in DI all the classes that implements some interface. Probably, in this case, you would do something like that:

var implementations = typeof(MyClass).Assembly.Types
   .Where(t => t.GetInterfaces().Any(i => i == typeof(IMyInterface))
foreach(var implementation in implementations)
{
   services.AddScoped<IMyInterface>(implementation);
}

And it would be completely fine. Even if Reflection is pretty slow, your application would not be getting significantly slower because of that. Certainly you would not notice the slow down.

For some other similar requirements some people used Fody or similar tool, that alters your assembly based on specified weavers. It works, though I always found if very confusing during debugging (because source code is no longer valid).

But we are not here to do the ‘ok’ job. We want better, more efficient code, that is generic but still close to native code. We strife for greatness!

Ok, maybe this is a bit too much, but following new .NET optimizations, we want to use new tools at our disposal to write simple but yet, efficient code.

We can do that by meta programming in C# and .NET using Roslyn Source Generators.

Source code generator

Writing your own source code generator is not that easy as it could be. Certainly it is a bit mode advanced topic and Microsoft is not putting as much money (and attention) into it as into new and groovy features that supposed to get you hook up into C#. But it should not stop you because it is also really cool seeing your source code generator generating new code on the fly, while you are making changes in your IDE. Seeing an error becoming valid code because code was generated in the background based on logic you coded is really great feeling!

In above video you can see my IDE, Rider, generating a code, class Hi with method Log generated on the fly based on new text file that is added to the project. First new Hi().Log() method call in the Program.cs is invalid, but when new txt file is added, source code generator generates new class, Program.cs becomes valid and when application is running, new line is written to the console. Pretty cool!

This may sound a bit complicated, and to some extent it is, but it probably won’t be most complicated thing you did or will do.

Creating new project

As with many C# examples, also in this, you have to create a new project.

This new project to be valid source code generator, it have to target .NET Standard 2.0. It is pretty simple to do with dropdown selection in your IDE or you can just change .csproj file directly:

<TargetFramework>netstandard2.0</TargetFramework>

Then mark your project as Roslyn component. You need to add following properties to <PropertyGroup> section in your project file:

<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>

After that install one package that helps you a bit with Roslyn APIs. For me personally they were a bit hard to understand when I first started working with them:

<PackageReference Include="HamsterWheel.FluentCodeGenerators" Version="0.4.0" PrivateAssets="all" />

You need to add also importing all of code generator Nuget packages to the output directory of your generator. Without it it will throw errors like below:

CSC : warning CS8784: Generator 'DemoIncrementalGenerator' failed to initialize. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly 'HamsterWheel.FluentCodeGenerators, Version=0.4.1.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified. [/builds/hamster-wheel/fluentcodegenerators/demo/HamsterWheel.FluentCodeGenerators.Demo.Use/HamsterWheel.FluentCodeGenerators.Demo.Use.csproj]

To fix that add following tags to your project file:

<Content Include="$(PKGHamsterWheel_FluentCodeGenerators)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="False" />
<Content Include="$(PKGHamsterWheel_FluentCodeGenerators_Abstractions)\lib\netstandard2.0\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="False" />

and also change the package reference tags:

<ItemGroup>
  <PackageReference Include="HamsterWheel.FluentCodeGenerators" Version="0.4.1" GeneratePathProperty="true" />
  <PackageReference Include="HamsterWheel.FluentCodeGenerators.Abstractions" Version="0.4.1" GeneratePathProperty="true" />
</ItemGroup>

This is enough to work on the Nuget package but if you want to develop your source code generator, it is much easier to have it in the same solution you want generate code for. To do this you need to do one additional change:

<PropertyGroup>
  <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>

<Target Name="GetDependencyTargetPaths">
  <ItemGroup>
     <TargetPathWithTargetPlatformMoniker Include="$(PKGHamsterWheel_FluentCodeGenerators_Abstractions)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false"/>
     <TargetPathWithTargetPlatformMoniker Include="$(PKGHamsterWheel_FluentCodeGenerators)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false"/>
  </ItemGroup>
</Target>

Above directives will make sure that all necessary packages, that are referenced by your source code generator project, are copied to appropriate directory from where your IDE and dotnet build are using your code generator .dll file. Without it it would be another FileNotFoundException during initialization.

After that your project should look similar too below:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>latestmajor</LangVersion>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
        <IsRoslynComponent>true</IsRoslynComponent>
        <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
        <IsPackable>false</IsPackable>
    </PropertyGroup>
    
    <ItemGroup>
        <PackageReference Include="HamsterWheel.FluentCodeGenerators" Version="0.4.1" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="HamsterWheel.FluentCodeGenerators.Abstractions" Version="0.4.1" GeneratePathProperty="true" PrivateAssets="all" />
    </ItemGroup>

    <PropertyGroup>
        <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
    </PropertyGroup>

    <Target Name="GetDependencyTargetPaths">
        <ItemGroup>
            <TargetPathWithTargetPlatformMoniker Include="$(PKGHamsterWheel_FluentCodeGenerators_Abstractions)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false"/>
            <TargetPathWithTargetPlatformMoniker Include="$(PKGHamsterWheel_FluentCodeGenerators)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false"/>
        </ItemGroup>
    </Target>

</Project>

Writing the generator

With project fully prepared we can jump to writing actual generator. First we need to add incremental generator class.

Lets start with following:

[Generator(LanguageNames.CSharp)]
public class DemoSolutionIncrementalGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
}

This is bare minimum code for incremental Roslyn source code generator. But as minimal starting point it does not do anything, so it is not quite useful. Yet.

Lets add following lines into Initializer method:

context.RegisterPostInitializationOutput(c =>
{
    c.AddSource("DummyFile.g.cs", """
                                  public class MyDummyGeneratedClassFile
                                  {
                                  }
                                  """);
});

This will instruct generator to emit single file called Dummy.g.cs with following content:

public class MyDummyGeneratedClassFile
{
}

This is just single, empty public class. Not very helpful, but as a starting demo is just fine.

This is good for very simple types that are to be added to the project before actual incremental generator is being used. In example if you coded you generator to enhance definition of user classes decorated by a specific attribute the flow would be:

  1. Your generator package is installed
  2. Incremental generator runs and RegisterPostInitializationOutput is executed.
  3. This will add generated attribute to user project
  4. User is coding new class using this attribute
  5. Your incremental generator is looking at source code for this specific attribute
  6. If any was found incremental source code generation is executed

This is how Regex code generators works with GeneratedRegex attribute.

Steps 5 and 6 require to use Roslyn API for obtaining IValueProvider. For example if we want to add new class for each of text files added to the project, like in demo above, we should add few line of code:

var additionalFilesProvider = context.AdditionalTextsProvider
            .Where(AdditionalTextPredicates.FileNameExtensionIs(".txt"))
            .Select(AdditionalTextSelectors.FileNamePathAndContent)
            .Collect();

This will instruct Roslyn to look for all the additional text files. Additional text is another type of file linked to the C# project via <AdditionalFiles Include="someFile.txt"/> project directive. Then Roslyn is looking in that collection for files that ends with txt extension. Then selects name of the file and its content. After that it is collected into single collection and fed to your source code generator.

But this is just a provider. Actual implementation of the class that logs additional file content to the console will have the template like below:

public class {{ File Name }}
{
   public void Log()
   {
       Console.WriteLine("{{ file content }}");
   }
}

Above template can be generated within code generator:

context.RegisterSourceOutput(additionalFilesProvider,
    (pc, files) =>
    {
        foreach (var file in files)
        {
            pc.AddSource(Path.GetFileNameWithoutExtension(file.FileName), $$"""
                                                                          public class {{Path.GetFileNameWithoutExtension(file.FileName)}}
                                                                          {
                                                                              public void Log()
                                                                              {
                                                                                  Console.WriteLine({{file.Content.TripleQuote()}});
                                                                              }
                                                                          }
                                                                          """);
        }
    });

Lets brake it down into steps:

  • It iterates through every additional file
    • for each one registers new generator output
    • each output is named the same as additional file is named
    • each output contains class that is named the same as additional file
    • each class contains Log() method that push additional file content to the console

This is totally fine for simple classes and it will work for them. Things starts to brake down a bit:

  • when you have to share logic between different classes.
  • when you need to make sure all used types are imported with using statement
  • when you need to balance multiple levels of different parenthesis
  • when you need to make sure everything is properly indented
  • when you need to make sure parameter names match, all ; are added and all returns are in proper places

When it is true, you have giant spaghetti of strings, interpolated strings, imports and methods calls.

It is getting even more complicated, when you have few dozens of files connected to the one generator logic (in example Hamster Wheel Source Generator project contains over 300 files!). Then you have to take into account that different logic can run in different places in different generated classes. Taking care of just the idea of type you are generating without taking care of specific C# syntax it will be using is much easier to wrap your head around.

This is where HamsterWheel.FluentCodeGenerator library is coming in. It helps with all of that and more.

In example above generator code can be rewritten as:

context.WithClass(Path.GetFileNameWithoutExtension(file.FileName),
    c => c.WithMethod("Log",
            m => m.WithBody(b => b.Append($"Console.WriteLine({file.Content.TripleQuote()});")))
);

This generator will emit very similar code:

[GeneratedCode("HamsterWheel.FluentCodeGenerators", "Version=0.4.2.0")]
public class Hi
{
    public void Log()
    {
        Console.WriteLine("""
        Hi!
        """);
    }
}

It adds GeneratedCode attribute to each type it generates so other parts of the system knows it was automatically generated code. Also it have better indentation in triple quoted strings.

Maybe it does not seems to be much but this package have much more features. My favorite is automated usings management. I can count how many times during development of mine code generators I had an error when some type was not imported. This is something your rarely keep count of when writing production code yourself. Usually it is done automatically by your IDE – you just write few letters and choose correct type from drop-down of IDE hints, you IDE takes over the rest. When you do something similar inside FluentCodeGenerators fluent API, it does something very similar. In example following code:

var ipAddressType = typeof(IPAdress);
classContext.WithProp<Type>("MyType", p => p.MakeComputed().WithExpressionBody(b => b.Append($"typeof({ipAddressType})")))

Generator will automatically add appropriate namespace using statement:

using System.CodeDom.Compiler;
using System.Net; //<-- this namespace was added automatically

[GeneratedCode("HamsterWheel.FluentCodeGenerators", "Version=0.4.1.0")]
public class MyClass
{
    public Type MyType => typeof(IPAddress);
}

This is done by having InterpolatedStringHandler being used instead of regular string. This way you can handle specific chunks of it and act differently when chunk is actual type.

Summary

Writing your own C# source code generator may look quite complicated at the beginning, when you are trying to figure it out. APIs are not very friendly (at least to me). When you try to find relevant docs (code comments in those APIs basically are not existent), you find out that MSDN docs about Roslyn code generators are lacking a bit in the usual quality.

But when you actually get a hand of it and won’t be scared by bugs in IDE or compiler pipeline – the end result is feels great! Especially with Fluent API.

HamsterWheel.FluentCodeGenerators is out

I released today first new Nuget package connected to my new project that I am working on for sometime: dynamically configurable, zero-downtime API, with low-code flows to write small pieces of logic – Hamster Wheel.

System heavily relies on dynamic source code generation. For that I wrote
https://www.nuget.org/packages/HamsterWheel.FluentCodeGenerators and https://www.nuget.org/packages/HamsterWheel.FluentCodeGenerators.Abstractions
small package that helps with writing C# incremental source code generators for Roslyn compiler.

Original Roslyn APIs are a bit hard to use so this package helps with that. Also it helps with:

– automatically emitting using statements
– formatting generated code
– with importing types during code generation
– with indentation and parenthesis balancing
– helps with adding and using method parameters in generated code
– `async` methods generation
– provides nicer to use wrappers to Roslyn `IncrementalValueProvider` and others
– allows to share pieces of code between files/classes (i.e. interfaces implementation)

More details are available here:
https://github.com/npodbielski/HamsterWheel.FluentCodeGenerators including readme with instructions of how to use it and a lot of examples of the features!

Convert LVM logical volume Raid 10 to Raid 5

Two weeks ago I was running out of space on my main raid matrix on my server. Server was configured and assembled few years back, and at that time I had about 700GB of data there so having RAID 10 of 4 1TB disks (that account for about 1,7TB of actual space for data) was enough. Especially when you count in a price of 2TB (or 4TB) NVME M2 disk back then.

But you hardly remove any data so in time it takes more and more of your hard disk space. And in this case adding another disk was not the quickest solution to a problem; quickest would be changing the raid type to get one 1TB of space more.

It is possible (in theory, according to LVM documentation). I just never done it and server is constantly in use so I did not want to go offline or login in single user mode to take volume or group offline first. Anyway here is how I did it.

  • first change RAID 10 to stripped.
  • convert stripped to raid5_n
  • convert raid5_n to raid5_ls
  • add 4th disk to raid

Convert RAID 10 to stripped

This is simple and immediate step. Just run:

lvconvert --type striped {{vg}}/{{lv}}

Of course replace {{vg}} and {{lv}} with actual name of your Volume Group and Logical Volume.

Convert stripped volume to RAID5_n

This is not immediate and LVM need time to convert volume. Of course time of this operation is very dependent on size of your disks and performance of your machine.

You need to run following command:

lvconvert --type raid5 {{vg}}/{{lv}}

Since this need time to be completed in the background you need to check from time to time progress of conversion. This can be done by running command:

 lvs -a -o name,copy_percent,devices {{vg}}

Look for value represented by Cp%Sync. When this is done it will 100.00.

Convert RAID5n to RAID5ls

Raid5_n in LVM is just a intermediate type of raid that is meant to be used to followup it to another type – it is not meant to be used for production scenarios. Just for conversion it is fine but if you want RAID5_ls is more appropriate. If you interested why, you can read about that in docs.

lvconvert --type raid5 {{vg}}/{{lv}}

This will by default convert logical volume to raid5_ls, which is more production ready. Again this will take time (and add 3 disk) so relax and check status from time to time running command:

 lvs -a -o name,copy_percent,devices {{vg}}

Add 4th disk to RAID5

By default converting stripped logical volume to RAID5 will create matrix of 3 disks. To add another one and have more space for you data you need instruct LVM to add another disk to matrix. It is possible running following command:

lvconvert --stripes 3 {{pv}}

This time LVM won’t choose physical device for you. You need to point out to the correct one, so change {{pv}} to name of your actual device. This should be something like /dev/sda (if you are using HDD or SDD disks) or /dev/nvmeXnY if you are using NVME disks.

This will take some time (again! but this is the last one!), but you can use your new space right away and LVM will synchronize all disks in the background so you do not need keep tabs on it.

Word of caution

One thing you need to be worry about: sometimes LVM is complaining about lacks of space between conversions, specifically converting stripped to raid5 may be problematic since it requires few extents for metadata (about 4mb on each disk if I remember correctly). If you have some another (not used physical device) it may use it for conversion. There is also possibility to use memory for conversion – but this is not persistent – and I am not sure what will be an outcome of such conversion to RAID10.

Generally for RAID conversions in LVM it is usually needed to have 4-6 extents of free space on each disk so make sure you leave some free space on each disk when you configure your logical volumes for first time – in example leave out 10 extents.

No Display Manager in Debian Trixie

Lately I decided to try Podman Quadlets. It seemed to be really interesting way to achieve similar results as with Docker and Compose, but it have nicer integration with the system (i.e. ability to see container in Cockpit along with other systemd services, or triggering container by socket, so less frequently used containers won’t be running all the time).

Everything went fine except for unavailability of buildah package in Debian stable apt channel (or the version that were needed for podman at least). Quick search on the internet and I found the answer is to reference this specific package from the testing channel of apt.

At this time I knew that I am going into problems willingly.

I was able to install buildah package and install podman and in few minutes have running container. Put my laptop to hibernation and went to sleep.

Next day however I was unable to boot my laptop correctly. Or it was booting but only to text mode, without Gnome Display Manager.

Starting display manager manually systemctl start gdm3, or gdmflexiserver or gdm3 were not doing any good. Also systemctl status display-manager was not helpful. All Gnome related services were dead, closed soon after the system started without any meaningful errors. journalctl also did not show any critical errors like missing GPU drivers or similar issues, but startx was working just fine by showing terminal emulator by using desktop application – so driver were fine.

In attempt to fix that by updating entirely to testing Debian, or Trixie i followed this article. But it updated several packages more and beside that did not help.

I thought that maybe changing to alternative Display Manager should fix that. Another web search showed that doing dpkg-reconfigure gdm3 should show configuration for choosing/changing display manager. But instead resulted in similar error (I am typing from memory).

gdm3 package was not installed correctly or some dependencies are missing.

At this point fix seemed straightforward: reinstall Gnome.

apt reinstall gdm3

proved to be successful by installing few Gnome packages like i.e. gnone-shell, after that desktop automatically were started and Debian switched from text based terminal to Gnome login screen.

PS: strangely I was able to install podman on another machine running also Debian bookwork without a problem.

Monitoring vs Observability

What is the difference between monitoring and observability?

Monitoring is process of collecting live events from your application, traces, debug messages, warnings and exceptions. The same process may involve sending those events to some storage or application that analyze or visualize those messages. I.e. in asp.net core applications monitoring of application involves calling a ILogger and sending those logs or critical exceptions to AppInsights. Monitoring may also involve activity of development team members to active watch those logged messages for any potential errors or problems that may impact availability of production environment.

Observability is on the other hand is analyzing messages produced by monitoring process to understand behavior of application and reason about it internal state, if this state is expected (correct behavior, ‘application works ok’) or may be unexpected (erroneous state, database is down, endpoints returns 500 etc). Typically involves dashboard with several metrics from monitoring, exceptions, response times, number of not OK responses in time, long database queries, number of active users etc (i.e. App Insights dashboard in asp.net core). Monitoring process of an application (as human activity) is typically using such observability dashboard.

So what is the difference between Observability and Monitoring? Observability is possible because Monitorin of an application was implemented. If metrics and logs would not be collected and stored system is not observable.

Basically Monitoring is collecting logs and metrics, while Observability is feature of the system enabled by such process.