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:
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.