Von Zeit zu Zeit, wenn ich über Roslyn und seine Analysegeräte las, hatte ich ständig den Gedanken: "Aber Sie können mit diesem Ding Nuget machen, das den Code umgeht und Code generiert." Eine schnelle Suche ergab nichts Interessantes, daher wurde beschlossen, zu graben. Wie angenehm war ich überrascht, als ich entdeckte, dass meine Idee nicht nur machbar war, sondern dass dies alles fast ohne Krücken funktionieren würde.
Und wer interessiert sich dafür, wie man ein "kleines Spiegelbild" machen und es bitte unter Katze in Nuget packen kann?
Einführung
Ich denke, das erste, was zu klären ist, ist, was unter "wenig Nachdenken" zu verstehen ist. Ich schlage vor, für alle Typen die Methode Dictionary<string, Type> GetBakedType()
, die die Namen der Eigenschaften und ihre Typen Dictionary<string, Type> GetBakedType()
. Da dies mit allen Typen funktionieren sollte, besteht die einfachste Möglichkeit darin, für jeden Typ eine Erweiterungsmethode zu generieren. Die manuelle Implementierung hat die folgende Form:
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; } }
Hier gibt es nichts Übernatürliches, aber es für alle Arten zu realisieren, ist ein trostloses und nicht interessantes Geschäft, das außerdem Tippfehler bedroht. Warum bitten wir den Compiler nicht um Hilfe? Hier betreten Roslyn und seine Analysatoren die Arena. Sie bieten die Möglichkeit, den Code zu analysieren und zu ändern. Lassen Sie uns dem Compiler einen neuen Trick beibringen. Lassen Sie ihn den Code durchgehen und sehen, wo wir ihn verwenden, aber unseren GetBakedType
noch nicht implementiert und implementiert haben.
Um diese Funktionalität zu "aktivieren", müssen wir nur ein Nuget-Paket installieren und alles wird sofort funktionieren. Rufen GetBakedType
als GetBakedType
bei Bedarf GetBakedType
auf. Wir erhalten einen Kompilierungsfehler, der besagt, dass die Reflektion für diesen Typ noch nicht fertig ist. Rufen Sie Codefix auf und Sie sind fertig. Wir haben eine Erweiterungsmethode, die alle öffentlichen Objekte an uns zurückgibt.
Ich denke, in der Bewegung wird es einfacher sein zu verstehen, wie es im Allgemeinen funktioniert. Hier ist eine kurze Visualisierung:
Wenn Sie dies lokal versuchen möchten, können Sie das Nuget-Paket unter dem Namen SimpleReflection installieren:
Install-Package SimpleReflection
Wer kümmert sich um die Quelle, sie sind hier .
Ich möchte warnen, dass diese Implementierung nicht für den tatsächlichen Gebrauch ausgelegt ist. Ich möchte nur einen Weg zeigen, wie die Codegenerierung mit Roslyn organisiert werden kann.
Vorbereitende Vorbereitung
Bevor Sie mit der Erstellung Ihrer Analysegeräte beginnen, müssen Sie die Komponente 'Visual Studio-Erweiterungsentwicklung' im Studio-Installationsprogramm installieren. Für VS 2019 müssen Sie daran denken, das ".NET Compiler Platform SDK" als optionale Komponente auszuwählen.
Analyzer-Implementierung
Ich werde nicht schrittweise beschreiben, wie der Analysator implementiert wird, da er sehr einfach ist, sondern nur die wichtigsten Punkte durchgehen.
Und der erste wichtige Punkt wird sein, dass wenn wir einen echten Kompilierungsfehler haben, die Analysatoren überhaupt nicht starten. Wenn wir versuchen, unseren GetBakedType()
im Kontext des Typs GetBakedType()
für den er nicht implementiert ist, wird ein Kompilierungsfehler angezeigt, und alle unsere Bemühungen sind nicht sinnvoll. Hier hilft uns jedoch die Kenntnis der Priorität, mit der der Compiler Erweiterungsmethoden aufruft. Der springende Punkt ist, dass bestimmte Implementierungen Vorrang vor generischen Methoden haben. Das heißt, im folgenden Beispiel wird die zweite Methode aufgerufen, nicht die erste:
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(); } }
Diese Funktion ist sehr praktisch. Wir definieren den generischen GetBakedType
einfach wie folgt:
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>(); } }
Dies ermöglicht es uns, einen Kompilierungsfehler am Anfang zu vermeiden und unseren eigenen Kompilierungs- "Fehler" zu generieren.
Betrachten Sie den Analysator selbst. Er wird zwei Diagnosen anbieten. Der erste ist für den Fall verantwortlich, dass die Codegenerierung überhaupt nicht gestartet wurde, und der zweite für den Fall, dass ein vorhandener Code aktualisiert werden muss. Sie haben die folgenden Namen SimpleReflectionIsNotReady
bzw. SimpleReflectionUpdate
. Die erste Diagnose generiert einen "Kompilierungsfehler" und die zweite meldet nur, dass Sie hier die Codegenerierung erneut ausführen können.
Die Diagnose wird wie folgt beschrieben:
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.");
Als nächstes muss festgelegt werden, wonach gesucht werden soll. In diesem Fall handelt es sich um einen Methodenaufruf:
public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); }
Dann gibt es bereits in HandelBuilder
eine Syntaxbaumanalyse. Am Eingang erhalten wir alle gefundenen Anrufe, sodass Sie alles außer unserem GetBakedType
. Dies kann mit dem üblichen Verfahren erfolgen, bei dem der Name der Methode überprüft wird. Als nächstes erhalten wir den Variablentyp, über den unsere Methode aufgerufen wird, und informieren den Compiler über die Ergebnisse unserer Analyse. Dies kann ein Kompilierungsfehler sein, wenn die Codegenerierung noch nicht gestartet wurde oder neu gestartet werden kann.
Es sieht alles so aus:
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); } }
Vollständiger Analysatorcode [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); } } }
Implementierung des Codegenerators
Wir werden die CodeFixProvider
über CodeFixProvider
, der unseren Analysator abonniert hat. Zunächst müssen wir überprüfen, was passiert ist, um unseren Analysator zu finden.
Es sieht so aus:
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); }
Alle Magie geschieht in CreateFormatterAsync
. Darin erhalten wir eine vollständige Beschreibung des Typs. Dann starten wir die Codegenerierung und fügen dem Projekt eine neue Datei hinzu.
Informationen abrufen und eine Datei hinzufügen:
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); }
Eigene Codegenerierung (Ich vermute, dass der Hub das gesamte Subnetz beschädigen wird):
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; }} }} "; }
Kompletter Codegenerator 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; }} }} "; } }
Zusammenfassung
Als Ergebnis haben wir einen Roslyn-Analysator-Code-Generator, mit dessen Hilfe eine "kleine" Reflexion unter Verwendung der Codegenerierung implementiert wird. Es wird schwierig sein, eine echte Anwendung für die aktuelle Bibliothek zu entwickeln, aber es wird ein gutes Beispiel für die Implementierung leicht zugänglicher Codegeneratoren sein. Dieser Ansatz kann wie jede Codegenerierung zum Schreiben von Serialisierern nützlich sein. Meine Testimplementierung von MessagePack funktionierte ~ 20% schneller als neuecc / MessagePack-CSharp , und ich habe noch keinen schnelleren Serializer gesehen. Darüber hinaus erfordert dieser Ansatz kein Roslyn.Emit
, das sich perfekt für Roslyn.Emit
und AOT-Szenarien Roslyn.Emit
.