Pembuatan Kode dengan Roslyn

Dari waktu ke waktu, ketika saya membaca tentang Roslyn dan analisanya, saya selalu berpikir: "Tetapi Anda dapat membuat nuget dengan hal ini, yang akan berkeliling kode dan melakukan pembuatan kode." Pencarian cepat tidak menunjukkan sesuatu yang menarik, jadi diputuskan untuk menggali. Betapa senangnya saya terkejut ketika saya menemukan bahwa ide saya tidak hanya layak, tetapi semua ini akan bekerja hampir tanpa kruk.


Jadi siapa yang tertarik untuk melihat bagaimana Anda bisa membuat "refleksi kecil" dan mengemasnya ke dalam nuget, tolong, di bawah kucing.


Pendahuluan


Saya pikir hal pertama yang harus diklarifikasi adalah apa yang dimaksud dengan "sedikit refleksi". Saya sarankan menerapkan untuk semua jenis metode Dictionary<string, Type> GetBakedType() , yang akan mengembalikan nama properti dan tipenya. Karena ini harus bekerja dengan semua jenis, opsi termudah adalah menghasilkan metode perluasan untuk setiap jenis. Implementasi manualnya akan memiliki bentuk sebagai berikut:


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

Tidak ada yang supranatural di sini, tetapi untuk merealisasikannya untuk semua jenis adalah bisnis yang suram dan tidak menarik yang, apalagi, mengancam kesalahan ketik. Mengapa kita tidak meminta bantuan kompiler. Di sinilah Roslyn dan analisanya memasuki arena. Mereka memberikan kesempatan untuk menganalisis kode dan mengubahnya. Jadi mari kita ajarkan pada kompiler trik baru. Biarkan dia berkeliling kode dan melihat di mana kita menggunakan, tetapi belum menerapkan GetBakedType kami dan mengimplementasikannya.


Untuk "mengaktifkan" fungsi ini, kita hanya perlu menginstal satu paket nuget dan semuanya akan segera berfungsi. Selanjutnya, panggil saja GetBakedType jika perlu, kami mendapatkan kesalahan kompilasi yang mengatakan bahwa refleksi untuk tipe ini belum siap, panggil kodefix dan Anda selesai. Kami memiliki metode ekstensi yang akan mengembalikan semua properti publik kepada kami.


Saya pikir dalam gerakan ini akan lebih mudah untuk memahami cara kerjanya secara umum, berikut adalah visualisasi singkat:



Jika Anda tertarik untuk mencoba ini secara lokal, Anda dapat menginstal paket nuget dengan nama SimpleReflection:


 Install-Package SimpleReflection 

Siapa yang peduli dengan sumbernya, mereka ada di sini .


Saya ingin memperingatkan implementasi ini tidak dirancang untuk penggunaan nyata. Saya hanya ingin menunjukkan cara untuk mengatur pembuatan kode dengan Roslyn.


Persiapan awal


Sebelum mulai membuat analisis, Anda harus menginstal komponen 'Pengembangan ekstensi Visual Studio' di pemasang studio. Untuk VS 2019, Anda harus ingat untuk memilih ".NET Compiler Platform SDK" sebagai komponen opsional.


Implementasi penganalisa


Saya tidak akan menjelaskan secara bertahap bagaimana menerapkan analisa, karena sangat sederhana, tetapi hanya melalui poin-poin kunci.


Dan poin kunci pertama adalah bahwa jika kita memiliki kesalahan kompilasi nyata, maka analisa tidak memulai sama sekali. Akibatnya, jika kami mencoba memanggil GetBakedType() dalam konteks tipe yang tidak diterapkan, kami akan mendapatkan kesalahan kompilasi dan semua upaya kami tidak masuk akal. Tetapi di sini kita akan terbantu oleh pengetahuan tentang prioritas yang digunakan kompilator untuk memanggil metode ekstensi. Intinya adalah implementasi spesifik lebih diutamakan daripada metode generik. Yaitu, dalam contoh berikut, metode kedua akan dipanggil, bukan yang pertama:


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

Fitur ini sangat berguna. Kami cukup mendefinisikan GetBakedType umum sebagai berikut:


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

Ini akan memungkinkan kami untuk menghindari kesalahan kompilasi di awal dan menghasilkan "kesalahan" kompilasi kami sendiri.


Pertimbangkan penganalisa itu sendiri. Ia akan menawarkan dua diagnostik. Yang pertama bertanggung jawab atas kasus ketika pembuatan kode tidak dimulai sama sekali, dan yang kedua ketika kita perlu memperbarui kode yang ada. Mereka akan memiliki nama-nama berikut SimpleReflectionIsNotReady dan SimpleReflectionUpdate masing-masing. Diagnostik pertama akan menghasilkan "kesalahan kompilasi", dan yang kedua hanya melaporkan bahwa di sini Anda dapat menjalankan pembuatan kode lagi.


Deskripsi diagnostik adalah sebagai berikut:


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

Selanjutnya, perlu untuk menentukan apa yang akan kita cari, dalam hal ini akan menjadi pemanggilan metode:


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

Maka sudah ada di HandelBuilder analisis pohon sintaks. Di pintu masuk, kami akan menerima semua panggilan yang ditemukan, jadi Anda harus membuang semuanya kecuali GetBakedType kami. Ini bisa dilakukan dengan cara biasa if kita mengecek nama metode tersebut. Selanjutnya kita mendapatkan jenis variabel yang disebut metode kita dan menginformasikan kepada kompiler tentang hasil analisis kami. Ini mungkin kesalahan kompilasi jika pembuatan kode belum dimulai atau kemampuan untuk memulainya kembali.


Itu semua terlihat seperti ini:


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

Kode analisa penuh
  [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); } } } 

Implementasi kode generator


Kami akan melakukan pembuatan CodeFixProvider melalui CodeFixProvider , yang berlangganan penganalisis kami. Pertama-tama, kita perlu memeriksa apa yang terjadi untuk menemukan penganalisa kita.


Ini terlihat seperti ini:


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

Semua keajaiban terjadi di dalam CreateFormatterAsync . Di dalamnya kita mendapatkan deskripsi lengkap tentang jenisnya. Kemudian kami memulai pembuatan kode dan menambahkan file baru ke proyek.


Mendapatkan informasi dan menambahkan file:


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

Pembuatan kode sendiri (saya menduga hub akan merusak seluruh subnet):


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

Pembuat kode lengkap
 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; }} }} "; } } 

Ringkasan


Sebagai hasilnya, kami memiliki generator kode analisis Roslyn dengan bantuan yang refleksi "kecil" menggunakan generasi kode diimplementasikan. Akan sulit untuk membuat aplikasi nyata untuk perpustakaan saat ini, tetapi itu akan menjadi contoh yang bagus untuk menerapkan generator kode yang mudah diakses. Pendekatan ini bisa, seperti pembuatan kode, berguna untuk menulis serialis. Implementasi pengujian saya terhadap MessagePack bekerja ~ 20% lebih cepat dari neuecc / MessagePack-CSharp , dan saya belum melihat serializer yang lebih cepat. Selain itu, pendekatan ini tidak memerlukan Roslyn.Emit , yang sangat cocok untuk skenario Unity dan AOT.

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


All Articles