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 = ;
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.
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.