Geração de código com Roslyn

De tempos em tempos, quando eu lia sobre Roslyn e seus analisadores, constantemente pensava: "Mas você pode fazer algo com essa coisa, que contorna o código e gera o código". Uma pesquisa rápida não mostrou nada de interessante, por isso foi decidido cavar. Quão agradavelmente fiquei surpreso quando descobri que minha idéia não era apenas viável, mas tudo isso funcionaria quase sem muletas.


E quem está interessado em ver como você pode fazer um "pequeno reflexo" e colocá-lo em uma pepita, por favor, embaixo de gato.


1. Introdução


Penso que a primeira coisa a esclarecer é o que se entende por "pouca reflexão". Sugiro implementar para todos os tipos o método Dictionary<string, Type> GetBakedType() , que retornará os nomes das propriedades e seus tipos. Como isso deve funcionar com todos os tipos, a opção mais fácil seria gerar um método de extensão para cada tipo. Sua implementação manual terá o seguinte formato:


 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; } } 

Não há nada sobrenatural aqui, mas perceber isso para todos os tipos é um negócio triste e não interessante que, além disso, ameaça erros de digitação. Por que não pedimos ajuda ao compilador. É aqui que Roslyn e seus analisadores entram na arena. Eles oferecem uma oportunidade para analisar o código e alterá-lo. Então, vamos ensinar um novo truque ao compilador. Deixe-o GetBakedType o código e ver onde usamos, mas ainda não implementamos nosso GetBakedType e o implementamos.


Para "habilitar" essa funcionalidade, precisamos instalar apenas um pacote de nuget e tudo funcionará imediatamente. Em seguida, basta chamar GetBakedType quando necessário, obteremos um erro de compilação que diz que o reflexo para esse tipo ainda não está pronto, chamar codefix e pronto. Temos um método de extensão que retornará todas as propriedades públicas para nós.


Eu acho que no movimento será mais fácil entender como geralmente funciona, aqui está uma breve visualização:



Se você estiver interessado em tentar isso localmente, poderá instalar o pacote nuget com o nome SimpleReflection:


 Install-Package SimpleReflection 

Quem se importa com a fonte, eles estão aqui .


Quero avisar que esta implementação não foi projetada para uso real. Eu só quero mostrar uma maneira de organizar a geração de código com Roslyn.


Preparação preliminar


Antes de começar a criar seus analisadores, você deve instalar o componente 'Desenvolvimento de extensão do Visual Studio' no instalador do estúdio. Para o VS 2019, lembre-se de selecionar o ".NET Compiler Platform SDK" como um componente opcional.


Implementação do analisador


Não descreverei em etapas como implementar o analisador, pois é muito simples, mas apenas analise os pontos principais.


E o primeiro ponto-chave será que, se houver um erro de compilação real, os analisadores não serão iniciados. Como resultado, se tentarmos chamar nosso GetBakedType() no contexto do tipo para o qual não está implementado, obteremos um erro de compilação e todos os nossos esforços não farão sentido. Mas aqui seremos ajudados pelo conhecimento da prioridade com a qual o compilador chama métodos de extensão. O ponto principal é que implementações específicas têm precedência sobre métodos genéricos. Ou seja, no exemplo a seguir, o segundo método será chamado, não o primeiro:


 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(); } } 

Esse recurso é muito útil. Simplesmente definimos o GetBakedType genérico da seguinte maneira:


 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>(); } } 

Isso nos permitirá evitar um erro de compilação no início e gerar nosso próprio "erro" de compilação.


Considere o próprio analisador. Ele oferecerá dois diagnósticos. O primeiro é responsável pelo caso em que a geração do código não foi iniciada e o segundo quando precisamos atualizar um código existente. Eles terão os seguintes nomes SimpleReflectionIsNotReady e SimpleReflectionUpdate respectivamente. O primeiro diagnóstico gerará um "erro de compilação" e o segundo apenas informa que aqui você pode executar a geração de código novamente.


A descrição dos diagnósticos é a seguinte:


  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."); 

Em seguida, é necessário determinar o que procuraremos, neste caso, será uma chamada de método:


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

Já no HandelBuilderHandelBuilder uma análise em árvore de sintaxe. Na entrada, receberemos todas as chamadas encontradas, então você precisa eliminar tudo, exceto nosso GetBakedType . Isso pode ser feito com o habitual if verificarmos o nome do método. Em seguida, obtemos o tipo de variável sobre a qual nosso método é chamado e informamos o compilador sobre os resultados de nossa análise. Esse pode ser um erro de compilação se a geração do código ainda não tiver sido iniciada ou a capacidade de reiniciá-lo.


Tudo se parece com isso:


 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); } } 

Código completo do analisador
  [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); } } } 

Implementação do gerador de código


Faremos a geração de CodeFixProvider por meio do CodeFixProvider , que é inscrito no nosso analisador. Antes de tudo, precisamos verificar o que aconteceu para encontrar nosso analisador.


É assim:


 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); } 

Toda mágica acontece dentro de CreateFormatterAsync . Nele, temos uma descrição completa do tipo. Começamos a geração do código e adicionamos um novo arquivo ao projeto.


Obtendo informações e adicionando um arquivo:


  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); } 

Geração de código próprio (suspeito que o hub interrompa toda a sub-rede):


 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; }} }} "; } 

Gerador de código completo
 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; }} }} "; } } 

Sumário


Como resultado, temos um gerador de código analisador Roslyn com a ajuda do qual é implementada uma reflexão "pequena" usando a geração de código. Será difícil criar um aplicativo real para a biblioteca atual, mas será um ótimo exemplo para implementar geradores de código facilmente acessíveis. Essa abordagem pode ser, como qualquer geração de código, útil para escrever serializadores. Minha implementação de teste do MessagePack funcionou ~ 20% mais rápido que o neuecc / MessagePack-CSharp , e ainda não vi um serializador mais rápido. Além disso, essa abordagem não requer o Roslyn.Emit , que é perfeito para os cenários Unity e AOT.

Source: https://habr.com/ru/post/pt455952/


All Articles