De vez en cuando, cuando le铆a sobre Roslyn y sus analizadores, pensaba constantemente: "Pero puedes hacer nuget con esta cosa, que dar谩 la vuelta al c贸digo y generar谩 c贸digo". Una b煤squeda r谩pida no mostr贸 nada interesante, por lo que se decidi贸 cavar. Qu茅 gratamente me sorprendi贸 cuando descubr铆 que mi idea no solo era factible, sino que todo esto funcionar铆a casi sin muletas.
Entonces, 驴qui茅n est谩 interesado en ver c贸mo puede hacer un "peque帽o reflejo" y empaquetarlo en nuget, por favor, debajo de cat.
Introduccion
Creo que lo primero que hay que aclarar es qu茅 se entiende por "poca reflexi贸n". Sugiero implementar para todos los tipos el m茅todo Dictionary<string, Type> GetBakedType()
, que devolver谩 los nombres de las propiedades y sus tipos. Como esto deber铆a funcionar con todos los tipos, la opci贸n m谩s f谩cil ser铆a generar un m茅todo de extensi贸n para cada tipo. Su implementaci贸n manual tendr谩 la siguiente forma:
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; } }
Aqu铆 no hay nada sobrenatural, pero realizarlo para todos los tipos es un asunto triste y no interesante que, adem谩s, amenaza los errores tipogr谩ficos. 驴Por qu茅 no le pedimos ayuda al compilador? Aqu铆 es donde Roslyn y sus analizadores entran en la arena. Brindan la oportunidad de analizar el c贸digo y cambiarlo. Entonces, ense帽emos al compilador un nuevo truco. Deje que GetBakedType
el c贸digo y vea d贸nde usamos, pero a煤n no hemos implementado nuestro GetBakedType
y lo implementa.
Para "habilitar" esta funcionalidad, solo necesitamos instalar un paquete nuget y todo funcionar谩 de inmediato. Luego, solo llame a GetBakedType
cuando sea necesario, obtenemos un error de compilaci贸n que dice que la reflexi贸n para este tipo a煤n no est谩 lista, llame a codefix y ya est谩. Tenemos un m茅todo de extensi贸n que nos devolver谩 todas las propiedades p煤blicas.
Creo que en el movimiento ser谩 m谩s f谩cil entender c贸mo funciona en general, aqu铆 hay una breve visualizaci贸n:
Si est谩 interesado en probar esto localmente, puede instalar el paquete nuget bajo el nombre SimpleReflection:
Install-Package SimpleReflection
A qui茅n le importa la fuente, est谩n aqu铆 .
Quiero advertir que esta implementaci贸n no est谩 dise帽ada para un uso real. Solo quiero mostrar una forma de organizar la generaci贸n de c贸digo con Roslyn.
Preparaci贸n preliminar
Antes de comenzar a hacer sus analizadores, debe instalar el componente 'Desarrollo de extensi贸n de Visual Studio' en el instalador del estudio. Para VS 2019, debe recordar seleccionar ".NET Compiler Platform SDK" como componente opcional.
Implementaci贸n de analizador
No describir茅 por etapas c贸mo implementar el analizador, ya que es muy simple, pero solo repase los puntos clave.
Y el primer punto clave ser谩 que si tenemos un error de compilaci贸n real, entonces los analizadores no comienzan en absoluto. Como resultado, si intentamos llamar a nuestro GetBakedType()
en el contexto del tipo para el que no est谩 implementado, obtendremos un error de compilaci贸n y todos nuestros esfuerzos no tendr谩n sentido. Pero aqu铆 seremos ayudados por el conocimiento de la prioridad con la que el compilador llama a los m茅todos de extensi贸n. El punto es que las implementaciones espec铆ficas tienen prioridad sobre los m茅todos gen茅ricos. Es decir, en el siguiente ejemplo, se llamar谩 al segundo m茅todo, no al primero:
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(); } }
Esta caracter铆stica es muy 煤til. Simplemente definimos el GetBakedType
gen茅rico de la siguiente manera:
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>(); } }
Esto nos permitir谩 evitar un error de compilaci贸n al principio y generar nuestro propio "error" de compilaci贸n.
Considere el analizador en s铆. Ofrecer谩 dos diagn贸sticos. El primero es responsable del caso en que la generaci贸n del c贸digo no comenz贸 en absoluto, y el segundo cuando necesitamos actualizar un c贸digo existente. Tendr谩n los siguientes nombres SimpleReflectionIsNotReady
y SimpleReflectionUpdate
respectivamente. El primer diagn贸stico generar谩 un "error de compilaci贸n", y el segundo solo informa que aqu铆 puede volver a ejecutar la generaci贸n de c贸digo.
La descripci贸n del diagn贸stico es la siguiente:
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.");
A continuaci贸n, es necesario determinar qu茅 buscaremos, en este caso ser谩 una llamada al m茅todo:
public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); }
Entonces ya en HandelBuilder
un an谩lisis de 谩rbol de sintaxis. En la entrada, recibiremos todas las llamadas encontradas, por lo que debe eliminar todo excepto nuestro GetBakedType
. Esto se puede hacer con lo habitual if
en el que verificamos el nombre del m茅todo. Luego obtenemos el tipo de variable sobre la cual se llama a nuestro m茅todo e informamos al compilador sobre los resultados de nuestro an谩lisis. Esto puede ser un error de compilaci贸n si la generaci贸n del c贸digo a煤n no se ha iniciado o la capacidad de reiniciarlo.
Todo se ve as铆:
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 analizador completo [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); } } }
Implementaci贸n de generador de c贸digo
CodeFixProvider
trav茅s de CodeFixProvider
, que est谩 suscrito a nuestro analizador. En primer lugar, debemos verificar qu茅 sucedi贸 para encontrar nuestro analizador.
Se ve as铆:
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 la magia ocurre dentro de CreateFormatterAsync
. En 茅l obtenemos una descripci贸n completa del tipo. Luego comenzamos la generaci贸n de c贸digo y agregamos un nuevo archivo al proyecto.
Obteniendo informaci贸n y agregando un archivo:
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); }
Generaci贸n de c贸digo propio (sospecho que el hub romper谩 toda la subred):
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; }} }} "; }
Generador 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; }} }} "; } }
Resumen
Como resultado, tenemos un generador de c贸digo de analizador Roslyn con la ayuda del cual se implementa una reflexi贸n "peque帽a" mediante la generaci贸n de c贸digo. Ser谩 dif铆cil encontrar una aplicaci贸n real para la biblioteca actual, pero ser谩 un gran ejemplo para implementar generadores de c贸digo f谩cilmente accesibles. Este enfoque puede ser, como cualquier generaci贸n de c贸digo, 煤til para escribir serializadores. Mi implementaci贸n de prueba de MessagePack funcion贸 ~ 20% m谩s r谩pido que neuecc / MessagePack-CSharp , y todav铆a no he visto un serializador m谩s r谩pido. Adem谩s, este enfoque no requiere Roslyn.Emit
, que es perfecto para los escenarios Unity y AOT.