New features of C × 9: code generator, compile time reflection

Keywords: C# Attribute encoding JSON xml

Preface

Today, the official blog of. NET announced the release of the first preview version of C 񖓿 9 source generators, a feature that users have been shouting for nearly 5 years, and finally released today.

brief introduction

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

With Source Generators, you can do these things:

  • Get a Compilation object, which represents all the compiled user code. 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 are executed as a phase in the compilation process:

Compile run - > analyze source code - > generate new code - > add the generated new code into the compilation process - > compile to continue.

In the above process, the bracketed content is the stage in which Source Generators participate and what they can do.

Effect

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

Compile time reflection - 0 runtime overhead

Taking ASP.NET Core as an example, when an ASP.NET Core application is started, the type definitions of Controllers, Services, etc. will be found first through runtime reflection, and then the constructor information of Controllers and Services needs to be obtained through runtime reflection in the request pipeline to facilitate dependency injection. However, the runtime reflection overhead is very large, even if the type signature is cached, it is not helpful for the application just started, and it is not conducive to AOT compilation.

Source Generators will enable all types discovery and dependency injection of ASP.NET Core to be completed and compiled into the final assembly at compile time, and finally achieve 0 runtime reflection, which is not only conducive to AOT compilation, but also 0 runtime overhead.

In addition to the above functions, gRPC and other functions can also use this function to weave code into the compilation to participate in compilation. No need to use any MSBuild Task to generate code again!

In addition, you can even read XML and generate C code directly from JSON to participate in compilation. It's OK to write DTO automatically.

AOT compilation

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

Many frameworks and libraries use reflection extensively, such as System.Text.Json, System.Text.RegularExpressions, ASP.NET Core, WPF, and so on, which discover types from user code at run time. These are not conducive to AOT compilation optimization, because in order for reflection to work properly, a large number of extra or even unnecessary type metadata must be compiled into the final native image.

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

Example

INotifyPropertyChanged

Anyone who has written WPF or UWP knows that in the ViewModel, in order to make property changes discoverable, the INotifyPropertyChanged interface needs to be implemented, and the property change event needs to be divided 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 more attributes, it will be very tedious. Previously, C introduced CallerMemberName to simplify the case when there are more 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, using the CallerMemberName indicator parameter, the caller's member name is automatically populated 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 the Source Generators, we need to write a type that implements the ISourceGenerator and annotates the 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 grammar sink that will be created each time it is generated
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }

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

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

            // Create property for 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 the Attribute of the new binding and INotifyPropertyChanged
            INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
            INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

            // Traverse the fields, and only the fields marked with AutoNotify will be preserved
            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 the field symbol information and save it if there is an AutoNotify annotation
                    IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
                    if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
                    {
                        fieldSymbols.Add(fieldSymbol);
                    }
                }
            }

            // Group fields by class and generate code
            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: must be at the top level to generate diagnostic information
                return null;
            }

            string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

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

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

            // Build 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;

            // Get 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>();

            // Called during compilation when each syntax node is accessed, we can check the node and save any useful information for generation
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                // Candidate any field with at least one Attribute
                if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax
                    && fieldDeclarationSyntax.AttributeLists.Count > 0)
                {
                    CandidateFields.Add(fieldDeclarationSyntax);
                }
            }
        }
    }
}

With the above code generator, we only need to write ViewModel in this way to automatically generate event trigger calls of notification interface:

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 for 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 using, use the Source Generators section as a stand-alone. NET Standard 2.0 assembly (2.1 is not supported temporarily), and introduce it 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>

Note that. NET 5 preview 3 or above is required, and specify the language version as preview:

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

In addition, Source Generators need 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 can't modify existing code. This is due to security considerations.

File

Source Generators is in the early preview stage, and there are no relevant documents on docs.microsoft.com. For its documents, please visit the documents in the roslyn warehouse:

Design documents

Working with documents

Epilogue

At present, the Source Generators are still in the very early preview stage, and the API may change a lot later, so they should not be used in production at this stage.

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

Posted by businessman332211 on Fri, 01 May 2020 14:48:51 -0700