A C# Source Generators, as Microsoft defines it, is “a new C# compiler feature that lets C# developers inspect user code and generate new C# source files that can be added to a compilation. This is done via a new kind of component that we’re calling a Source Generator.”
Think of C# Source generator as your colleague who writes C# code at compile time from some meta-data or from existing type inspection or some external information source (e.g. text files, config files, resource files, etc..).
Source generators run as a phase of compilation as visualized below (source: Microsoft):
C# Source Generator can generate new source code at compile time, but it can not change existing one. This means that you can not change the body of the method or change other structures from existing classes.
Before digging into code, let’s take at few examples where this awesome feature can be very useful.
Example #1: Getting assembly info – Reflection vs. C# Source Generation version
Reflection is very popular and useful mechanism to retrieve application type system metadata at runtime. On the other hand, Reflection is generally very slow. And, if possible, should be avoided – at least on snappy, fast applications.
Yet, in almost every C# application you can find this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ using System; using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Jenx.CsSourceCodeGenerator")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] [assembly: System.Reflection.AssemblyProductAttribute("Jenx.CsSourceCodeGenerator")] [assembly: System.Reflection.AssemblyTitleAttribute("Jenx.CsSourceCodeGenerator")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] // Generated by the MSBuild WriteCodeFragment class. |
AssemblyInfo class is using Reflection to retrieve meta information of the executing assembly.
All this info is also available at compile time. Moreover, all this data can be collected at compile time and stored/persisted in some collection or similar. This way, performance could be enhanced quite a bit.
This is exactly how ThisAssembly works. At compile time it uses C# Source Generator to collect metadata of the assembly, store it for later – runtime usage. Runtime only gets this data from “static cached in-code storage”, there is no performance degradation as with Reflection.
Example #2: Compile Time Dependency Injection For C#
Generally, Dependency Injection heavily rely on the Reflection. It resolves registered types at runtime, creates new instances and inject them into new objects via constructors or properties. As said, all this is done by Reflection, so performance hit is normally very huge. Therefore, many existing DI containers/injectors suffers for performance issues.
On the contrary, Strong Type Inject does compile time Dependency Injection for C# by using C# Source Generators. There is no type Container dictionary lookups, no Reflection, no runtime type resolving. All is done at compile time. Application speed is greatly improved. Furthermore, type checking is done at compile, which is far safer then runtime type checking.
Here I presented only two cases how C# ecosystem can benefit from C# Source Generators. This feature is new in Roslyn compiler, let’s wait and see how developers will accept and use it.
Let’s do some coding
To start experimenting with C# Source Generators, I created solution with two projects in Visual Studio (16.8+):
- NETStandard 2.0 class library which will contain C# Code generators and
- Consuming library (in my case .NET 5 Console app ) which will consume source code generated by C# generators in my class library.
C# Source Generator class library
First, to create C# Source Generators, I used Visual Studio template for creating new .NETStandard 2.0 class library. Next, I marked my generator class with Generator attribute. Moreover, my class must implement ISourceGenerator interface. And…that’s all. My C# Source Generator is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System; using System.Text; namespace Jenx.CsSourceCodeGenerators { [Generator] public class JenxSimpleWelcomeGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { var myAutoGeneratedCode = $@" using System; namespace Jenx.CompileTimeGenerated {{ public static class JenxWelcome {{ public static void SayHello() {{ Console.WriteLine(""My personal welcome""); Console.WriteLine(""*********************""); Console.WriteLine(""Hello from generated code from jenx.si""); Console.WriteLine(""App compile date/time: { DateTime.Now }""); Console.WriteLine(""App excute date/time: "" + DateTime.Now.ToString()); Console.WriteLine(""Compiled on: { System.Net.Dns.GetHostName() } machine""); Console.WriteLine(""App executed on: "" + System.Net.Dns.GetHostName() + "" machine""); }} }} }}"; context.AddSource("jenxWelcome", SourceText.From(myAutoGeneratedCode, Encoding.UTF8)); } public void Initialize(GeneratorInitializationContext context) { } } } |
It’s straightforward cognitive flow :): this class is C# Source Generator which will generate C# source code at compile time, inject it into my executing application by C# compiler. It will be compiled into intermediate language (IL) and executed by Common Language Runtime.
All this functionality is contained in two NuGet packages, which I reference in my project:
To make things even clearer, my project now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.8.0" /> </ItemGroup> </Project> |
My C# Source Generator is ready. Let’s check how can I use it.
Consuming app, .NET 5 Console
For my purpose, I created very simple .NET 5 Console application.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; namespace Jenx.CsSourceCodeGeneratorConsumer { internal class Program { private static void Main(string[] args) { CompileTimeGenerated.JenxWelcome.SayHello(); Console.ReadLine(); } } } |
Obviously, I don’t have CompileTimeGenerated
class in my project. Here is the magic, this class will be injected and compiled via my NETStandard class library containing my source generator.
There is one additional thing to do: when referencing project with C# Source Generators I need to put into ProjectReference MSBuild/csproj element two additional attributes OutputItemType="Analyzer"
and ReferenceOutputAssembly="false"
. E.g.:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net5.0</TargetFramework> <AssemblyName>Jenx.CsSourceCodeGeneratorConsumer</AssemblyName> <RootNamespace>Jenx.CsSourceCodeGeneratorConsumer</RootNamespace> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\Generator\Jenx.CsSourceCodeGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup> </Project> |
First attribute instructs compiler that referencing project is source generator (basically Code Analyzer). Second attribute instructs C# compiler not to include compiled dll into output – this is due a fact that source generator inject code into consuming assembly. Thus, there is no need for this assembly to go to application output . Later on, I will show disassembled code and all this will be a little more clearer.
Anyway, I use Visual Studio as my IDE. In Visual Studio I can see my project with source generator also in the Analyzers section, as shown below.
Ok, everything is on place, I compile my solution and run my app. Here is the output:
For you, dear reader, below I attach runtime screenshot of my app running on some 3rd Windows machine. You can observe interesting DateTime
and HostName
code magic/execution.
Interesting output, right?
As I promised before, this is my disassembled code:
Disassembler shows how nicely my code was emitted into my consuming application by my C# Source Generator. Super awesome!
Read file and generate C# code
As an extra, in the next section, I will present C# Source Generator which will read content of the text file and generate source code based on the content of that file.
In my Console application, I will add new text file named “Person.txt” with the following content:
1 2 3 4 5 6 |
John Jane Lucas Carlos Mary George |
Very important: in Solution Explorer I set build action for this file to “C# analyzer additional file”.
And now, the most important part: my C# Source Generated class which will read text file and dynamically – at compile time – generate C# source code and send it to compilation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using System; using System.Linq; using System.Text; namespace Jenx.CsSourceCodeGenerators { [Generator] public class JenxSimpleFileWelcomeGenerator : ISourceGenerator { public void Execute(GeneratorExecutionContext context) { var helloFromAllString = ""; var txtFiles = context.AdditionalFiles.Where(at => at.Path.EndsWith(".txt")); foreach (AdditionalText file in txtFiles) { string textFileContext = file.GetText(context.CancellationToken).ToString(); var textLines = textFileContext.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); foreach (var name in textLines) helloFromAllString += $@" Console.WriteLine(""Hi from {name}"");" + Environment.NewLine; } var myAutoGeneratedCode = $@" using System; namespace Jenx.CompileTimeGenerated {{ public static class PersonsWelcome {{ public static void SayHelloFromAll() {{ Console.WriteLine(""Guys, say hello:""); Console.WriteLine(""*********************""); {helloFromAllString} }} }} }}"; context.AddSource("jenxWelcomeFromAll", SourceText.From(myAutoGeneratedCode, Encoding.UTF8)); } public void Initialize(GeneratorInitializationContext context) { } } } |
Consuming of this is class is simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
using System; namespace Jenx.CsSourceCodeGeneratorConsumer { internal class Program { private static void Main(string[] args) { CompileTimeGenerated.JenxWelcome.SayHello(); Console.ReadLine(); // executing my second C# Source Generator... CompileTimeGenerated.PersonsWelcome.SayHelloFromAll(); Console.ReadLine(); } } } |
Output of my application after adding my second source generator:
Below you can see disassembled code. C# code is generated and executed – no runtime reading of the file or anything.
That’s it. I presented two simple C# Source Generators. These two generators are quite simple, but they are good examples to start with and to understand C# Source Generators.
Summary
In my opinion, C# Source Generators is one of the best features introduced into Roslyn C# compiler (as of 16.8 Preview 3).
Nevertheless, it can not change existing code, but with some tricks (e.g. partial methods), emitting generated code at compile time can get very powerful. It can greatly enrich your application building at compile time.
Don’t wait, try C# Source Generators. It’s really super awesome feature of the latest C# compiler.
Happy coding.