[Update] Creating Nostalgic AOP Using Source Generator
Tags: roslyn, dotnet
Published: 2024-01-15 06:00:29

# Introduction to Roslyn Source Generator

[Roslyn Source Generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview) is used in various repositories, such as [dotnet-isolated-worker](https://github.com/Azure/azure-functions-dotnet-worker). Unfortunately, I have never worked with it and couldn't understand the code. So, I decided to write a simple sample application to try and understand it.

The challenge is to create a mock version of AOP (Aspect Oriented Programming). I wanted to see if I could write a sample that injects logs before and after the execution of a function based on defined attributes.

# What are Roslyn APIs?

Source Generator is part of the [Roslyn APIs (The .NET Compiler Platform SDK)](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/). The Roslyn APIs allow the evaluation of source code during compilation to create a model of the application. These APIs are used to manipulate that model. Specifically, the following can be done:

* Static code analysis
* Code fixes
* Code refactoring
* Code generation

By using these features, Visual Studio can provide code suggestions and assist with code refactoring. This feature enables support for maintaining coding conventions.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/dc023221-8955-01ba-599b-dd306fc3a07f.png)

.NET executes a pipeline similar to the one shown above during compilation. APIs are provided to support this pipeline:

* **Syntax Tree API**: Access the tree model created by parsing the source code.
* **Symbol API**: Access the symbols created as a result of parsing the code.
* **Binding and Flow Analysis APIs**: Bind symbols to identifiers in the code.
* **Emit API**: Output assemblies.

Comparing what Visual Studio can do with Roslyn APIs makes it easier to understand the capabilities.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/12599c60-2401-6e7a-b74f-b7d9c74071ee.png)

# Source Generator

For this investigation, I want to focus on [Source Generator](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview).

It allows you to perform the following steps:

* Access the Compilation object to access and analyze the structure of the code.
* Generate source code and compile it to create an assembly.

Specifically, you can:

* Improve performance by performing tasks that are typically done with reflection using code generation.
* Manipulate the order of execution of tasks in MSBuild.
* Convert dynamic routing into static routing using code generation (used by ASP.NET).

Some use cases I have come across are:

- Creating a definition file for logging is cumbersome, so perform code analysis and generate patterns for all of them.
- Improve performance by generating code for methods that should be executed dynamically, referring to attributes.

## How to use it

### Create a source generator project

Reference the following libraries. Note that it seems to work only with `netstandard2.0` currently.

```XML:SourceGenerator.csproj
<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<TargetFramework>netstandard2.0</TargetFramework>
	</PropertyGroup>

	<ItemGroup>
		<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" PrivateAssets="all" />
		<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
	</ItemGroup>
</Project>
```

### Create a generator

To create a generator, implement the `ISourceGenerator` interface. You need to implement the `Initialize(GeneratorInitializationContext context)` and `Execute(GeneratorExecutionContext context)` methods. You can access the current code model and perform code generation through the `context` objects in each method. You can access the existing model using the `context.Compilation` object, and perform code generation using the `context.AddSource` method.

You can read, analyze the model, generate code according to your preferences, and compile it. In the following example, a factory is automatically created.

```csharp:FactoryGenerator.cs
namespace SourceGenerator
{
    [Generator]
    public class FactoryGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            // How to access the CompilationObject
            INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName("AOPLib.FunctionAttribute");
            // Create Factory
            context.AddSource("FunctionFactory.g.cs", SourceText.From($@"
namespace AOPLib{{
  public class FunctionFactory {{
    public static IFunction CreateFunction(string name) {{
          switch(name) {{
            default:
              return new SampleFunction();
          }}
    }}
  }}
}}
", Encoding.UTF8));
        }

        public void Initialize(GeneratorInitializationContext context)
        {

        }
```

The caller would look like the following. We haven't created the code for the `FunctionFactory` class anywhere, but there will be no complaints from the IDE about the missing class.

```csharp
    public static void Main(string[] args)
    {
        var functionName = Console.ReadLine();
        IFunction function = AOPLib.FunctionFactory.CreateFunction(functionName);
        function.Execute(new FunctionContext() { Name = "hello", TraceId = Guid.NewGuid() });
    }
```

In other words, the project containing the original `Program.cs` file, which acts as the caller, would have the following project definition. By setting `OutputItemType=Analyzer` and `ReferenceOutputAssembly="false"`, the SourceGenerator project's DLL is not imported, and the analyzer is executed. You can find detailed definitions in [Common MSBuild Project Items](https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-items?view=vs-2022#projectreference).

```XML:Sample.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\SourceGenerator\SourceGenerator.csproj"
					  OutputItemType="Analyzer"
					  ReferenceOutputAssembly="false"/>
  </ItemGroup>

</Project>
```

This is the basic usage of Source Generator.

# Debugging with Source Generator

## Debugging

By default, it is not possible to debug Source Generators directly. However, you can enable debugging in Visual Studio by calling the following method in the `Initialize` method:

```csharp
public void Initialize(GeneratorInitializationContext context)
{
    Debugger.Launch();
}
```

## Viewing Generated Source

By default, you cannot view the source code generated by the generator. However, you can enable it by adding the following definition to the `csproj` file of the project where the `Program.cs` is located. Add it to the `PropertyGroup` section:

```XML:csproj
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/a3c6486b-e374-0009-9b16-ee526ddf643a.png)

# Creating an AOP Sample

The previous example was a simple one, but it would be better to try a slightly more complex sample. So let's implement a fake AOP (Aspect-Oriented Programming) by defining an attribute called `Function`. If the `Logging` flag in the attribute is set to `true`, it automatically outputs logs when the method is executed.

For example, if we write a program like this:

```csharp:LoggingFunction.cs
namespace AOPSample
{
    [Function(isLogging: true)]
    public class LoggingFunction : IFunction
    {
        public void Execute(FunctionContext context)
        {
            Console.WriteLine($"[{nameof(LoggingFunction)}]: execute. Name: {context.Name}");
        }
    }
}
```

Since the `isLogging` attribute is set to `true`, the Source Generator will automatically find this file and inject the logging logic.

To make it easier, let's write an adapter like this:

```csharp:LoggingAdapter.cs
public class LoggingAdapter : IFunction
{
    private readonly IFunction _function;
    public LoggingAdapter(IFunction function)
    {
        _function = function;
    }
    public void Execute(FunctionContext context)
    {
        Console.WriteLine($"[{DateTime.UtcNow}] {_function.GetType().Name}: before execute. TraceId: {context.TraceId}");
        _function.Execute(context);
        Console.WriteLine($"[{DateTime.UtcNow}] {_function.GetType().Name}: after execute. TraceId: {context.TraceId}");
    }
}
```

Then, you can generate the code by creating an instance of `LoggingAdapter` and passing it to `new ***Function()`.

## Code Generation Logic

```csharp:FactoryGenerator.cs
namespace SourceGenerator
{
    [Generator]
    public class FactoryGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            // Get the symbol for the FunctionAttribute
            INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName("AOPLib.FunctionAttribute");
            // Get all classes from Compilation.SyntaxTrees
            var targetClasses = context.Compilation.SyntaxTrees
                .SelectMany(st => st.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>());
            // Extract only the classes that have the FunctionAttribute
            var selectedTargetClasses = targetClasses.Where(p => p.AttributeLists.FirstOrDefault()?.Attributes.FirstOrDefault()?.Name.NormalizeWhitespace().ToFullString() == "Function");
            // Save information about the selected classes as metadata in a dictionary
            var registeredClasses = new Dictionary<string, FunctionMetadata>();
            // Check the information of the Attribute used in the class.
            // Store the metadata information of the Function in the dictionary, such as `isLogging`.
            foreach (var clazz in selectedTargetClasses)
            {
                var attribute = clazz.AttributeLists.First()?.Attributes.FirstOrDefault();
                var attributeArgument = attribute.ArgumentList.Arguments.FirstOrDefault();
                var isLogging = attributeArgument.Expression.NormalizeWhitespace().ToFullString().Contains("true");
                var typeSymbol = context.Compilation.GetSemanticModel(clazz.SyntaxTree).GetDeclaredSymbol(clazz);
                var className = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
                var classNameOnly = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
                registeredClasses.Add(classNameOnly, new FunctionMetadata { FullyQualifiedName = className, Name = classNameOnly, IsLogging = isLogging });
            }

            // Create the code generation template and generate the code
            context.AddSource("FunctionFactory.g.cs", SourceText.From($@"
namespace AOPLib{{
  public class FunctionFactory {{
    public static IFunction CreateFunction(string name) {{
          switch(name) {{
            {GetFunctionsSection(registeredClasses)}
            default:
              return new SampleFunction();
          }}
    }}
  }}
}}
", Encoding.UTF8));
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            Debugger.Launch();
        }

        private string GetFunctionsSection(Dictionary<string, FunctionMetadata> metadata)
        {
            var sb = new StringBuilder();
            foreach (var item in metadata)
            {
                sb.AppendLine($"case \"{item.Key}\":");
                if (item.Value.IsLogging)
                {
                    sb.AppendLine($"return new LoggingAdapter(new {item.Value.FullyQualifiedName}());");
                }
                else
                {
                    sb.AppendLine($"return new {item.Value.FullyQualifiedName}();");
                }
            }
            return sb.ToString();
        }
    }

    class FunctionMetadata
    {
        public string Name { get; set; }
        public string FullyQualifiedName { get; set; }
        public bool IsLogging { get; set; }
    }
}
```

### Execution Result

Although no code was written in the main program, logs are automatically outputted as expected with the fake AOP.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/9689fbcd-82d8-90da-6d37-8118ae554246.png)

If `isLogging` is set to `false`, no logs will be outputted.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470/088a4a86-cb06-f20c-b372-2afd3d62c71f.png)

# IncrementalGenerators

Now, the interface I used in the above example is an official method, but it seems to be an older method. I received feedback from someone with worldwide expertise. It's amazing to receive feedback from such people when I write a blog!

https://x.com/neuecc/status/1703657375394341330?s=20

You should probably read the article [C# in 2022 (Incremental) Source Generator Development Method](https://neue.cc/2022/12/16_IncrementalSourceGenerator.html) and the official documentation on [Incremental Generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md). Let's explain the new interface.

## IIncrementalGenerator

Compared to `ISourceGenerator`, it only has
## Porting the AOP Sample

Now that we have learned the new approach, let's rewrite the AOP sample with `Incremental Generators`.
Great! It has become much easier to understand! I will add explanations within the code. The process follows the same flow as explained earlier: we process the `IncrementalValue(s)Provider<T>` using a Linq-like interface, manipulate it, and pass the final result to `RegisterOutput` to generate the files. The key point is to realize that it may look like Linq, but it's not actually Linq. By correctly reading about [Incremental Generators](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md), you will understand that it's not about handling collections, so it can also return a single value. That's where `IncrementalValueProvider<T>` comes in for singular values, and `IncrementalValuesProvider<T>` for multiple values.

I will add comments as needed throughout the code.

```csharp
namespace SourceGenerator
{
    [Generator(LanguageNames.CSharp)]
    public class FactoryGenerator : IIncrementalGenerator
    {
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            // Uncomment this line if you want to debug.
            // Debugger.Launch();
            // ExtensionMethod ForAttributeWithMetadataName() is awesome!
            // It returns all classes with the specified attribute.
            var source = context.SyntaxProvider.ForAttributeWithMetadataName(
                "AOPLib.FunctionAttribute",
                (node, token) => true,
                (ctx, token) => ctx
                )
                // It can be written in a similar way to Linq. Here we analyze the classes and 
                // retrieve the values from the attributes attached to the classes and store 
                // them in FunctionMetadata.
                .Select((s, token) => { 
                    var typeSymbol = (INamedTypeSymbol)s.TargetSymbol;
                    var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
                    var name = typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
                    var isLogging = (bool)(s.Attributes.FirstOrDefault()?.ConstructorArguments.FirstOrDefault().Value ?? false);
                    return new FunctionMetadata { FullyQualifiedName = fullType, Name = name, IsLogging = isLogging };
            // Finally, we perform Collect() to convert it to IncrementalValueProvider.
            // It was IncrementalValuesProvider<FunctionMetadata> before, but now it
            // has been converted to IncrementalValueProvider<ImmutableArray<FunctionMetadata>>.
            // We convert it from plural to singular to ensure the Action is only called once.
            }).Collect();

            context.RegisterSourceOutput(source, Emit);
        }

        // Since we have converted it to a singular form, this method will be called only once.
        // Without Collect(), it would be called multiple times.
        private void Emit(SourceProductionContext context, ImmutableArray<FunctionMetadata> source)
        {
            // Create Factory
            context.AddSource("FunctionFactory.g.cs", SourceText.From($@"
namespace AOPLib{{
  public class FunctionFactory {{
    public static IFunction CreateFunction(string name) {{
          switch(name) {{
            {GetFunctionsSection(source)}
            default:
              return new SampleFunction();
          }}
    }}
  }}
}}
", Encoding.UTF8));
        }

        private string GetFunctionsSection(ImmutableArray<FunctionMetadata> metadata)
        {
            var sb = new StringBuilder();
            foreach (var item in metadata)
            {
                sb.AppendLine($"case \"{item.Name}\":");
                if (item.IsLogging)
                {
                    sb.AppendLine($"return new LoggingAdapter(new {item.FullyQualifiedName}());");
                }
                else
                {
                    sb.AppendLine($"return new {item.FullyQualifiedName}();");
                }
            }
            return sb.ToString();
        }
    }

    class FunctionMetadata
    {
        public string Name { get; set; }
        public string FullyQualifiedName { get; set; }
        public bool IsLogging { get; set; }
    }
}
```

Compared to the previous version, it looks much cleaner. By the way, let's compare the case where `IIncrementalGenerator` is available and when it's not, like this:

* [With IIncrementalGenerator](https://github.com/TsuyoshiUshio/AOPSample/tree/a0b9eeba3958fc7b4b1c8686db5797fd8e0ac334)
* [Without IIncrementalGenerator](https://github.com/TsuyoshiUshio/AOPSample/tree/b432bc952cd2556e2f59d96e707e146359cee634)

# Summary
This generator enables us to execute code that would normally be done using reflection in a static manner, or analyze the structure of the code. It is a very useful and interesting feature. However, I have only scratched the surface, and I think it may be difficult to detect bugs in the generated code. It seems that it would be necessary to write unit tests or other means to cover those cases.

~~However, since I am still new to this, I can't say that I fully understand it yet. I will continue to investigate and write more samples.~~


I have provided the sample used in this blog post here:

* [AOP Sample](https://github.com/TsuyoshiUshio/AOPSample/tree/main)

# Resources
* [Source Generation Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md)
* [Source Generator Design document](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md)
* [Official Samples](https://github.com/dotnet/roslyn-sdk/tree/main/samples/CSharp/SourceGenerators)

This blog is translated from [[更新] Source Generator を使って懐かしの AOP を作ってみる](https://qiita.com/TsuyoshiUshio@github/items/b42773afaa4a25c2af60) Powered by [BlogBabel](https://github.com/TsuyoshiUshio/BlogBabel)