Kami menerapkan AutoMapper menggunakan Roslyn dan pembuatan kode

Dalam artikel sebelumnya , saya menjelaskan cara mengatur pembuatan kode dengan Roslyn. Tugas saat itu adalah menunjukkan pendekatan bersama. Sekarang saya ingin mewujudkan sesuatu yang akan memiliki aplikasi nyata.


Jadi, siapa pun yang tertarik melihat bagaimana Anda bisa membuat perpustakaan seperti AutoMapper meminta kucing.


Pendahuluan


Pertama-tama, saya pikir ada baiknya menggambarkan bagaimana Ahead of Time Mapper (AOTMapper) saya akan bekerja. Titik masuk mapper kita akan menjadi metode extention generik MapTo<> . Penganalisis akan mencarinya dan menawarkan untuk menerapkan MapToUser ekstensi MapToUser , di mana User adalah tipe yang diteruskan ke MapTo<> .


Sebagai contoh, ambil kelas berikut:


 namespace AOTMapper.Benchmark.Data { public class UserEntity { public UserEntity() { } public UserEntity(Guid id, string firstName, string lastName) { this.Id = id; this.FirstName = firstName; this.LastName = lastName; } public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } public class User { public string FirstName { get; set; } public string LastName { get; set; } public string Name { get; set; } } } 

MapToUser dihasilkan akan terlihat seperti ini:


 public static AOTMapper.Benchmark.Data.User MapToUser(this AOTMapper.Benchmark.Data.UserEntity input) { var output = new AOTMapper.Benchmark.Data.User(); output.FirstName = input.FirstName; output.LastName = input.LastName; output.Name = ; // missing property return output; } 

Seperti yang dapat Anda lihat dari contoh ini, semua properti dengan nama dan tipe yang sama ditetapkan secara otomatis. Pada gilirannya, yang tidak ada kecocokan ditemukan terus "hang" menciptakan kesalahan kompilasi dan entah bagaimana pengembang harus menanganinya.


Misalnya, seperti ini:


 public static AOTMapper.Benchmark.Data.User MapToUser(this AOTMapper.Benchmark.Data.UserEntity input) { var output = new AOTMapper.Benchmark.Data.User(); output.FirstName = input.FirstName; output.LastName = input.LastName; output.Name = $"{input.FirstName} {input.LastName}"; return output; } 

Selama pembuatan MapToUser , MapTo<User> panggilan MapTo<User> akan digantikan oleh MapToUser .


Cara kerjanya dalam gerak dapat dilihat di sini:



AOTMapper juga dapat diinstal melalui nuget:


 Install-Package AOTMapper 

Kode proyek lengkap dapat ditemukan di sini .


Implementasi


Saya berpikir untuk waktu yang lama bagaimana hal ini dapat dilakukan secara berbeda dan pada akhirnya saya sampai pada kesimpulan bahwa ini tidak terlalu buruk, karena ini menyelesaikan beberapa ketidaknyamanan yang menyiksa saya ketika menggunakan AutoMapper .


Pertama, kami mendapatkan metode ekstensi yang berbeda untuk jenis yang berbeda, sebagai akibatnya, untuk beberapa jenis User abstrak, kami dapat dengan mudah menggunakan IntelliSense untuk mengetahui peta mana yang sudah diterapkan tanpa harus mencari file yang sama di mana peta kami terdaftar. Lihat saja metode ekstensi apa yang sudah Anda miliki.


Kedua, pada saat runtime itu hanya metode ekstensi dan karenanya kami menghindari semua overhead yang terkait dengan memanggil mapper kami. Saya mengerti bahwa pengembang AutoMapper menghabiskan banyak upaya untuk mengoptimalkan panggilan, tetapi masih ada beberapa biaya tambahan. Tolok ukur kecil saya menunjukkan bahwa rata-rata 140-150ns per panggilan, tidak termasuk waktu inisialisasi. Benchmark itu sendiri dapat dilihat di repositori, dan hasil pengukuran lebih rendah.


MetodeBerartiKesalahanStddevKejadian 0Kejadian 1Kejadian 2Dialokasikan
AutoMapperToUserEntity151,84 ns1,9952 ns1,8663 ns0,0253--80 B
AOTMapperToUserEntity10,41 ns0,2009 ns0,1879 ns0,0152--48 B
AutoMapperToUser197,51 ns2,9225 ns2.5907 ns0,0787--248 B
AOTMapperToUser46,46 ns0,3530 ns0,3129 ns0,0686--216 B

Selain itu, keuntungan dari mapper ini termasuk fakta bahwa umumnya tidak memerlukan waktu untuk menginisialisasi ketika aplikasi dimulai, yang dapat berguna dalam aplikasi besar.


Alat analisis itu sendiri memiliki bentuk berikut (tidak ada kode yang mengikat):


 private void Handle(OperationAnalysisContext context) { var syntax = context.Operation.Syntax; if (syntax is InvocationExpressionSyntax invocationSytax && invocationSytax.Expression is MemberAccessExpressionSyntax memberAccessSyntax && syntax.DescendantNodes().OfType<GenericNameSyntax>().FirstOrDefault() is GenericNameSyntax genericNameSyntax && genericNameSyntax.Identifier.ValueText == "MapTo") { var semanticModel = context.Compilation.GetSemanticModel(syntax.SyntaxTree); var methodInformation = semanticModel.GetSymbolInfo(genericNameSyntax); if (methodInformation.Symbol.ContainingAssembly.Name != CoreAssemblyName) { return; } var fromTypeInfo = semanticModel.GetTypeInfo(memberAccessSyntax.Expression); var fromTypeName = fromTypeInfo.Type.ToDisplayString(); var typeSyntax = genericNameSyntax.TypeArgumentList.Arguments.First(); var toTypeInfo = semanticModel.GetTypeInfo(typeSyntax); var toTypeName = toTypeInfo.Type.ToDisplayString(); var properties = ImmutableDictionary<string, string>.Empty .Add("fromType", fromTypeName) .Add("toType", toTypeName); context.ReportDiagnostic(Diagnostic.Create(AOTMapperIsNotReadyDescriptor, genericNameSyntax.GetLocation(), properties)); } } 

Yang dia lakukan adalah memeriksa apakah itu adalah metode yang kita butuhkan, ekstrak tipe dari entitas tempat MapTo<> dipanggil bersama-sama dari parameter pertama metode umum, dan menghasilkan pesan diagnostik.


Pada gilirannya akan diproses di dalam AOTMapperCodeFixProvider . Di sini kita mendapatkan informasi tentang tipe-tipe yang akan kita jalankan pembuatan kode. Kemudian kami mengganti panggilan ke MapTo<> dengan implementasi tertentu. Kemudian kita memanggil AOTMapperGenerator yang akan menghasilkan file dengan metode ekstensi.


Dalam kode tersebut, tampilannya seperti ini:


 private async Task<Document> Handle(Diagnostic diagnostic, CodeFixContext context) { var fromTypeName = diagnostic.Properties["fromType"]; var toTypeName = diagnostic.Properties["toType"]; var document = context.Document; var semanticModel = await document.GetSemanticModelAsync(); var root = await diagnostic.Location.SourceTree.GetRootAsync(); var call = root.FindNode(diagnostic.Location.SourceSpan); root = root.ReplaceNode(call, SyntaxFactory.IdentifierName($"MapTo{toTypeName.Split('.').Last()}")); var pairs = ImmutableDictionary<string, string>.Empty .Add(fromTypeName, toTypeName); var generator = new AOTMapperGenerator(document.Project, semanticModel.Compilation); generator.GenerateMappers(pairs, new[] { "AOTMapper", "Mappers" }); var newProject = generator.Project; var documentInNewProject = newProject.GetDocument(document.Id); return documentInNewProject.WithSyntaxRoot(root); } 

AOTMapperGenerator sendiri memodifikasi proyek yang masuk dengan membuat file dengan pemetaan antar tipe.
Ini dilakukan sebagai berikut:


 public void GenerateMappers(ImmutableDictionary<string, string> values, string[] outputNamespace) { foreach (var value in values) { var fromSymbol = this.Compilation.GetTypeByMetadataName(value.Key); var toSymbol = this.Compilation.GetTypeByMetadataName(value.Value); var fromSymbolName = fromSymbol.ToDisplayString().Replace(".", ""); var toSymbolName = toSymbol.ToDisplayString().Replace(".", ""); var fileName = $"{fromSymbolName}_To_{toSymbolName}"; var source = this.GenerateMapper(fromSymbol, toSymbol, fileName); this.Project = this.Project .AddDocument($"{fileName}.cs", source) .WithFolders(outputNamespace) .Project; } } private string GenerateMapper(INamedTypeSymbol fromSymbol, INamedTypeSymbol toSymbol, string fileName) { var fromProperties = fromSymbol.GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .ToDictionary(o => o.Name, o => o.Type); var toProperties = toSymbol.GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .ToDictionary(o => o.Name, o => o.Type); return $@" public static class {fileName}Extentions {{ public static {toSymbol.ToDisplayString()} MapTo{toSymbol.ToDisplayString().Split('.').Last()}(this {fromSymbol.ToDisplayString()} input) {{ var output = new {toSymbol.ToDisplayString()}(); { toProperties .Where(o => fromProperties.TryGetValue(o.Key, out var type) && type == o.Value) .Select(o => $" output.{o.Key} = input.{o.Key};" ) .JoinWithNewLine() } { toProperties .Where(o => !fromProperties.TryGetValue(o.Key, out var type) || type != o.Value) .Select(o => $" output.{o.Key} = ; // missing property") .JoinWithNewLine() } return output; }} }} "; } 

Kesimpulan


Secara total, kami memiliki mapper yang berfungsi saat menulis kode, dan tidak ada lagi runtime yang tersisa. Paket datang dengan cara untuk menambahkan kemampuan konfigurasi. Misalnya, konfigurasikan templat untuk nama metode yang dihasilkan dan tentukan direktori tempat menyimpan. Juga tambahkan kemampuan untuk melacak perubahan tipe. Saya punya ide bagaimana ini bisa diatur, tetapi saya menduga ini mungkin terlihat dalam hal konsumsi sumber daya, dan sejauh ini telah diputuskan untuk menunda ini.

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


All Articles