في مقالة سابقة ، وصفت طريقة لتنظيم توليد الشفرات مع Roslyn. وكانت المهمة آنذاك لإظهار نهج مشترك. الآن أريد أن أدرك شيئًا سيكون له تطبيق حقيقي.
وهكذا ، فإن أي شخص مهتم بالنظر في كيفية إنشاء مكتبة مثل AutoMapper يطلب القط.
مقدمة
بادئ ذي بدء ، أعتقد أنه من المفيد وصف كيف ستعمل أداة تعيين Ahead of Time (AOTMapper) الخاصة بي. ستكون نقطة الدخول الخاصة MapTo<>
طريقة MapTo<>
العامة MapTo<>
. سيبحث المحلل عن ذلك MapToUser
تطبيق MapToUser
تمديد MapToUser
، حيث يكون User
هو النوع الذي يتم تمريره إلى MapTo<>
.
كمثال ، خذ الطبقات التالية:
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
كما يلي:
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 = ;
كما ترى من هذا المثال ، يتم تلقائيًا تعيين جميع الخصائص التي تحمل نفس الأسماء والأنواع. وبدورها ، تستمر العناصر "التي لم يتم العثور على نتائج مطابقة لها" في "تعليق" لإنشاء خطأ في الترجمة ويجب على المطور التعامل معها بطريقة أو بأخرى.
على سبيل المثال ، مثل هذا:
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; }
أثناء MapToUser
، سيتم استبدال MapTo<User>
استدعاء MapTo<User>
بـ MapToUser
.
كيف يعمل في الحركة يمكن أن ينظر إليه هنا:
يمكن أيضًا تثبيت AOTMapper عبر nuget:
Install-Package AOTMapper
يمكن العثور على رمز المشروع الكامل هنا .
تطبيق
لقد فكرت لفترة طويلة كيف يمكن القيام بذلك بشكل مختلف وفي النهاية توصلت إلى استنتاج مفاده أن هذا ليس سيئًا للغاية ، لأن هذا يحل بعض الإزعاج الذي عذبني عند استخدام AutoMapper
.
أولاً ، نحصل على طرق تمديد مختلفة لأنواع مختلفة ، ونتيجة لذلك ، بالنسبة لبعض أنواع User
المجردة ، يمكننا بسهولة استخدام IntelliSense لمعرفة الخرائط التي تم تنفيذها بالفعل دون الاضطرار إلى البحث عن نفس الملف حيث يتم تسجيل خرائطنا. مجرد إلقاء نظرة على ما تمديد طرق لديك بالفعل.
ثانياً ، في وقت التشغيل هو مجرد وسيلة تمديد وبالتالي نتجنب أي النفقات العامة المرتبطة استدعاء مخطط لدينا. أدرك أن مطوري AutoMapper
بذلوا الكثير من الجهد لتحسين الاتصال ، لكن لا تزال هناك بعض التكاليف الإضافية. أظهر مؤشري الصغير أنه في المتوسط يتراوح بين 140 و 150 نانومتر لكل مكالمة ، باستثناء وقت التهيئة. يمكن عرض المعيار نفسه في المستودع ، وتكون نتائج القياس أقل.
بالإضافة إلى ذلك ، تتضمن ميزات هذا المخطط حقيقة أنه لا يتطلب عمومًا وقتًا لبدء التهيئة عند بدء تشغيل التطبيق ، مما قد يكون مفيدًا في التطبيقات الكبيرة.
يحتوي المحلل نفسه على النموذج التالي (يفتقد رمز الربط):
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)); } }
كل ما يفعله هو التحقق مما إذا كانت هذه هي الطريقة التي نحتاجها ، واستخراج النوع من الكيان الذي يتم استدعاء MapTo<>
من المعلمة الأولى للطريقة المعممة ، ويقوم بإنشاء رسالة تشخيصية.
بدوره سيتم معالجتها داخل AOTMapperCodeFixProvider
. هنا نحصل على معلومات حول الأنواع التي سنعمل عليها لإنشاء الشفرة. ثم نستبدل الدعوة إلى MapTo<>
بتطبيق محدد. ثم ندعو AOTMapperGenerator
والتي سوف تولد ملف مع طريقة التمديد.
في الكود ، يبدو كما يلي:
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
نفسه يعدل المشروع الوارد عن طريق إنشاء ملفات مع تعيينات بين الأنواع.
ويتم ذلك على النحو التالي:
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; }} }} "; }
النتائج
إجمالًا ، لدينا مخطط يعمل بشكل صحيح في وقت كتابة التعليمات البرمجية ، ثم يبقى أي شيء في وقت تشغيله. الخطط تأتي مع وسيلة لإضافة قدرات التكوين. على سبيل المثال ، قم بتكوين القوالب الخاصة بأسماء الطرق التي تم إنشاؤها وحدد الدليل الذي تريد حفظه. أضف أيضًا القدرة على تتبع التغييرات في الأنواع. لدي فكرة عن كيفية تنظيم ذلك ، لكنني أظن أن هذا قد يكون ملحوظًا من حيث استهلاك الموارد ، وقد تقرر حتى الآن تأخير ذلك.