Roslyn的代码生成

时不时地,当我读到有关Roslyn及其分析仪的信息时,我总是有这样的想法:“但是,您可以用这个东西来制造nuget,它将围绕代码并进行代码生成。” 快速搜索没有发现任何有趣的内容,因此决定进行挖掘。 当我发现我的想法不仅可以实现,而且几乎不用拐杖就能工作时,我感到非常惊喜。


因此,有兴趣的人可以看看如何进行“小反射”并将其包装到nuget中。


引言


我认为首先要澄清的是“小反射”的含义。 我建议为所有类型实现Dictionary<string, Type> GetBakedType()方法,该方法将返回属性名称及其类型。 由于这适用于所有类型,因此最简单的选择是为每种类型生成扩展方法。 它的手动实现将具有以下形式:


 using System; using System.Collections.Generic; public static class testSimpleReflectionUserExtentions { private static Dictionary<string, Type> properties = new Dictionary<string, Type> { { "Id", typeof(System.Guid)}, { "FirstName", typeof(string)}, { "LastName", typeof(string)}, }; public static Dictionary<string, Type> GetBakedType(this global::testSimpleReflection.User value) { return properties; } } 

这里没有什么超自然的东西,但是要实现所有类型的东西都是沉闷而不是有趣的事情,而且还会威胁到错别字。 我们为什么不向编译器寻求帮助。 这是罗斯林和他的分析仪进入竞技场的地方。 它们提供了分析和更改代码的机会。 因此,让我们教一个新的技巧。 让他浏览一下代码,看看我们在哪里使用,但尚未实现并实现我们的GetBakedType


要“启用”此功能,我们只需要安装一个nuget软件包,一切就可以立即使用。 接下来,仅在必要时调用GetBakedType ,我们会收到一个编译错误,指出该类型的反射尚未准备好,请调用codefix,然后完成。 我们有一个扩展方法,它将所有公共属性返回给我们。


我认为在运动中将更容易理解其工作原理,以下是简短的可视化效果:



有兴趣在本地尝试此操作的任何人都可以以SimpleReflection名称安装nuget软件包:


 Install-Package SimpleReflection 

谁在乎消息源,他们在这里


我要警告此实现并非为实际使用而设计。 我只想展示一种使用Roslyn组织代码生成的方法。


初步准备


在开始制造分析仪之前,必须在Studio安装程序中安装组件“ Visual Studio扩展开发”。 对于VS 2019,您必须记住选择“ .NET Compiler Platform SDK”作为可选组件。


分析仪实施


因为它非常简单,所以我不会分阶段介绍如何实现分析器,而只是简单地介绍了关键点。


第一个关键点是,如果我们遇到真正的编译错误,那么分析器将根本无法启动。 结果,如果尝试在未实现该类型的上下文中调用GetBakedType() ,则会遇到编译错误,并且我们的所有努力都将变得毫无意义。 但是在这里,编译器调用扩展方法的优先级将为我们提供帮助。 重点在于,特定的实现优先于通用方法。 也就是说,在下面的示例中,将调用第二种方法,而不是第一种:


 public static class SomeExtentions { public static void Save<T>(this T value) { ... } public static void Save(this User user) { ... } } public class Program { public static void Main(string[] args) { var user = new User(); user.Save(); } } 

此功能非常方便。 我们只需按如下方式定义通用GetBakedType


 using System; using System.Collections.Generic; public static class StubExtention { public static Dictionary<string, Type> GetBakedType<TValue>(this TValue value) { return new Dictionary<string, Type>(); } } 

这将使我们从一开始就避免编译错误,并生成我们自己的编译“错误”。


考虑分析仪本身。 他将提供两个诊断。 第一个原因是代码生成根本没有开始的情况,第二个原因是我们需要更新现有代码的情况。 它们将分别具有以下名称: SimpleReflectionIsNotReadySimpleReflectionUpdate 。 第一个诊断将生成“编译错误”,第二个仅报告在此处可以再次运行代码生成。


诊断说明如下:


  public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); 

接下来,有必要确定我们要寻找的内容,在这种情况下,它将是一个方法调用:


 public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } 

然后,在HandelBuilder已经进行了语法树分析。 在入口处,我们将收到找到的所有呼叫,因此您需要清除除GetBakedType之外的GetBakedTypeif我们在其中检查方法的名称,可以使用通常的方法来完成。 接下来,我们获得调用其方法的变量的类型,并将分析结果告知编译器。 如果代码生成尚未开始或无法重新启动,则可能是编译错误。


一切看起来像这样:


 private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType") { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } 

完整的分析器代码
  [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SimpleReflectionAnalyzer : DiagnosticAnalyzer { public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(SimpleReflectionIsNotReadyDescriptor, SimpleReflectionUpdateDescriptor); public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType" ) { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } } 

代码生成器实现


我们将通过CodeFixProvider进行CodeFixProvider生成,该CodeFixProvider已订阅我们的分析器。 首先,我们需要检查发生了什么事情才能找到我们的分析仪。


看起来像这样:


 public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } 

所有魔术都发生在CreateFormatterAsync内部。 在其中,我们获得了该类型的完整描述。 然后,我们开始代码生成并将新文件添加到项目中。


获取信息并添加文件:


  private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } 

自己的代码生成(我怀疑集线器会破坏整个子网):


 private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } 

完整的代码生成器
 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using SimpleReflection.Utils; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace SimpleReflection { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SimpleReflectionCodeFixProvider)), Shared] public class SimpleReflectionCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(SimpleReflectionAnalyzer.SimpleReflectionIsNotReady, SimpleReflectionAnalyzer.SimpleReflectionUpdate); public sealed override FixAllProvider GetFixAllProvider() { return WellKnownFixAllProviders.BatchFixer; } public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var symbolName = symbol.ToDisplayString(); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } } 

总结


结果,我们有了一个Roslyn分析器代码生成器,借助它,可以实现使用代码生成的“小”反射。 很难为当前库提供一个真正的应用程序,但这将是实现易于访问的代码生成器的一个很好的例子。 像任何代码生成一样,此方法对于编写序列化程序可能很有用。 我的MessagePack测试实现比neuecc / MessagePack-CSharp快20%,并且我还没有看到更快的序列化程序。 此外,此方法不需要Roslyn.Emit ,它非常适合Unity和AOT方案。

Source: https://habr.com/ru/post/zh-CN455952/


All Articles