C # source code generators

Original link: https://www.cnblogs.com/hez2010/p/12810993.html

preface

today. NET official blog announced the release of the first preview version of C# 9 Source Generators. This is a feature that users have been shouting for nearly five years. It was finally released today.

brief introduction

As its name implies, Source Generators allows developers to obtain and view user code and generate new C# code during code compilation, participate in the compilation process, and can be well integrated with the code analyzer to provide Intellisense, debugging information and error reporting information, which can be used for code generation, Therefore, it is also equivalent to an enhanced version of compile time reflection.

Using Source Generators, you can do these things:

  • Get a Compilation object, which represents all the user code being compiled. You can get AST, semantic model and other information from it
  • You can insert new code into the Compilation object and let the compiler compile with the existing user code

Source Generators is executed as a phase in the compilation process:

Compile and run - > analyze source code - > generate new code - > add the generated new code to the compilation process - > compilation continues.

In the above process, the contents in brackets are the stages and things that Source Generators can do.

effect

. NET clearly has the functions of runtime reflection and dynamic IL weaving. What's the use of this Source Generators?

Compile time reflection - 0 runtime overhead

Take ASP Net core example, start an ASP Net core application, the type definitions of Controllers, Services, etc. will be found through runtime reflection, and then the constructor information needs to be obtained through runtime reflection in the request pipeline to facilitate dependency injection. However, the overhead of runtime reflection is very large. Even if the type signature is cached, it will not help the application just started, and it is not conducive to AOT compilation.

Source Generators will enable ASP Net core all type discovery, dependency injection, etc. are completed at compile time and compiled into the final assembly. Finally, 0 runtime reflection is used, which is not only conducive to AOT compilation, but also has 0 runtime overhead.

In addition to the above functions, gRPC and others can also use this function to weave in code to participate in compilation during compilation. There is no need to use any MSBuild Task for code generation!

In addition, you can even read XML and JSON and directly generate C# code to participate in compilation. There is no problem with the full automation of DTO writing.

AOT compilation

Another function of Source Generators is to help eliminate the main obstacles to AOT compilation optimization.

Reflection is heavily used in many frameworks and libraries, such as system Text. Json,System.Text.RegularExpressions,ASP.NET Core and WPF, etc., which find types from user code at run time. These are very disadvantageous to AOT compilation optimization, because in order for reflection to work properly, a large number of additional and possibly unnecessary type metadata must be compiled into the final native image.

With Source Generators, you only need to do compile time code generation to avoid most of the use of runtime reflection, so that AOT compilation optimization tools can run better.

example

INotifyPropertyChanged

Those who have written WPF or UWP know that in ViewModel, in order to make property changes discoverable, it is necessary to implement the INotifyPropertyChanged interface and trigger property change events at the setter of each required property:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Text)));
        }
    }
}

When there are many attributes, it will be very cumbersome. Previously C# introduced CallerMemberName to simplify the case when there are many attributes:

class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            OnPropertyChanged();
        }
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

That is, the CallerMemberName parameter is used to indicate that the caller's member name is automatically filled in at compile time.

But it's still inconvenient.

Now with Source Generators, we can generate code at compile time to do this.

In order to implement Source Generators, we need to write an ISourceGenerator that implements ISourceGenerator and mark the type of Generator.

The complete Source Generators code is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace MySourceGenerator
{
    [Generator]
    public class AutoNotifyGenerator : ISourceGenerator
    {
        private const string attributeText = @"
using System;
namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        {
        }
        public string PropertyName { get; set; }
    }
}
";

        public void Initialize(InitializationContext context)
        {
            // Register a syntax receiver, which will be created each time it is generated
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

        public void Execute(SourceGeneratorContext context)
        {
            // add to Attrbite text
            context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));

            // Get previous syntax sink 
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;

            // Create an attribute for the target name at
            CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
            Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options));

            // Get new bound Attribute,And get INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

            // Traverse the field, leaving only AutoNotify Annotated fields
            List<IFieldSymbol> fieldSymbols = new List<IFieldSymbol>();
            foreach (FieldDeclarationSyntax field in receiver.CandidateFields)
            {
                SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree);
                foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables)
                {
                    // Get field symbol information, if any AutoNotify Dimensions are saved
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }

            // Press class Group fields and generate codes
            foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(f => f.ContainingType))
            {
                string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
               context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
            }
        }

        private string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context)
        {
            if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
            {
                // TODO: Diagnostic information must be generated at the top level
                return null;
            }

            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

            // Start building the code to be generated
            StringBuilder source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");

            // If the type has not been implemented INotifyPropertyChanged Add implementation
            if (!classSymbol.Interfaces.Contains(notifySymbol))
            {
                source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
            }

            // Generate properties
            foreach (IFieldSymbol fieldSymbol in fields)
            {
                ProcessField(source, fieldSymbol, attributeSymbol);
            }

            source.Append("} }");
            return source.ToString();
        }

        private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
        {
            // Get field name
            string fieldName = fieldSymbol.Name;
            ITypeSymbol fieldType = fieldSymbol.Type;

            // obtain AutoNotify Attribute And related data
            AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
            TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value;

            string propertyName = chooseName(fieldName, overridenNameOpt);
            if (propertyName.Length == 0 || propertyName == fieldName)
            {
                //TODO: Unable to process, generating diagnostic information
                return;
            }

            source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}
    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}
");

            string chooseName(string fieldName, TypedConstant overridenNameOpt)
            {
                if (!overridenNameOpt.IsNull)
                {
                    return overridenNameOpt.Value.ToString();
                }

                fieldName = fieldName.TrimStart('_');
                if (fieldName.Length == 0)
                    return string.Empty;

                if (fieldName.Length == 1)
                    return fieldName.ToUpper();

                return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
            }

        }

        // Syntax sink, which will be created on demand each time code is generated
        class SyntaxReceiver : ISyntaxReceiver
        {
            public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

            // It is called when accessing each syntax node in compilation. We can check the node and save any useful information for generation
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // Will have at least one Attribute Any field of the as a candidate
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

After having the above code generator, we only need to write ViewModel in this way in the future, and the event trigger call of the notification interface will be automatically generated:

public partial class MyViewModel
{
    [AutoNotify]
    private string _text = "private field text";

    [AutoNotify(PropertyName = "Count")]
    private int _amount = 5;
}

The above code will automatically generate the following code during compilation:

public partial class MyViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

    public string Text
    {
        get 
        {
            return this._text;
        }
        set
        {
            this._text = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Text)));
        }
    }

    public int Count
    {
        get 
        {
            return this._amount;
        }
        set
        {
            this._amount = value;
            this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Count)));
        }
    }
}

Very convenient!

When used, the Source Generators section is treated as a separate NET Standard 2.0 assembly (2.1 is not supported temporarily) can be introduced into your project in the following ways:

<ItemGroup>
  <Analyzer Include="..\MySourceGenerator\bin\$(Configuration)\netstandard2.0\MySourceGenerator.dll" />
</ItemGroup>

<ItemGroup>
  <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj" />
</ItemGroup>

Pay attention to the need for the latest NET 5 preview (there was no formal release in artifacts when writing the article), and specify the language version as preview:

<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

In addition, Source Generators needs to introduce two nuget packages:

<ItemGroup>
  <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.final" PrivateAssets="all" />
  <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
</ItemGroup>

limit

Source Generators can only be used to access and generate code, but cannot modify existing code. This is due to security considerations.

file

Source Generators is in the early preview stage, docs microsoft. There are no relevant documents on. Com. Please visit the documents in roslyn warehouse:

Design documents

Working with documents

Postscript

Current Source Generators Already in system Text. Put into use in JSON 6.0.

In addition, the development of integration with IDE, diagnosis information, breakpoint debugging information, etc. is also in progress. Please look forward to the subsequent preview version.

 

Keywords: C#

Added by Axariel on Sun, 06 Mar 2022 18:47:23 +0200