نحن تكييف AutoMapper لأنفسنا

يعد AutoMapper أحد الأدوات الرئيسية المستخدمة في تطوير تطبيقات Enterprise ، لذلك أريد أن أكتب أقل قدر ممكن من التعليمات البرمجية أثناء تحديد تعيين الكيانات.


أنا لا أحب الازدواجية في MapFrom مع توقعات واسعة.


CreateMap<Pupil, PupilDto>() .ForMember(x => x.Name, s => s.MapFrom(x => x.Identity.Passport.Name)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Surname)) .ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.Number, s => s.MapFrom(x => x.Identity.Passport.Number)) 

أرغب في إعادة كتابته بالشكل التالي:


 CreateMap<Pupil, PupilDto>() .From(x=>x.IdentityCard.Passport).To() 

ProjectTo


يمكن أن ينشئ AutoMapper تعيينًا في الذاكرة وترجمته إلى SQL ، ويكمل التعبير ، ويعرض في DTO وفقًا للقواعد التي وصفتها في ملفات التعريف.


 EntityQueryable.Select(dtoPupil => new PupilDto() { Name = dtoPupil.Identity.Passport, Surname = dtoPupil.Identity.Passport.Surname}) 

80٪ من التعيين الذي يجب علي كتابته هو التعيين الذي يكمل التعبير من IQueryble.


هذا مريح جدا:


 public ActionResult<IEnumerable<PupilDto>> GetAdultPupils(){ var result = _context.Pupils .Where(x=>x.Identity.Passport.Age >= 18 && ...) .ProjectTo<PupilDto>().ToList(); return result; } 

في نمط التصريح ، قمنا بتكوين استعلام على جدول التلاميذ ، وأضاف التصفية ، وإسقاطها في DTO المطلوب وإعادته إلى العميل ، حتى تتمكن من كتابة جميع طرق القراءة لواجهة CRUD بسيطة ، وسيتم كل ذلك على مستوى قاعدة البيانات.


صحيح ، في تطبيقات خطيرة مثل هذه الإجراءات من غير المرجح أن ترضي العملاء.


سلبيات AutoMapper'a


1) إنه أمر مطول للغاية ، مع تعيين "واسع" عليك كتابة القواعد التي لا تتلاءم مع سطر واحد من التعليمات البرمجية.


تنمو ملفات التعريف وتتحول إلى أرشيفات للكود الذي يتم كتابته مرة واحدة ويتغير فقط عند إعادة تكوين الأسماء.


2) إذا كنت تستخدم التعيين وفقًا للاتفاقية ، يتم فقد الاسم
العقارات في DTO:


 public class PupilDto { //  Pupil       IdentityCard // IdentityCard     Passport public string IdentityCardPassportName { get; set; } public string IdentityCardPassportSurname { get; set; } } 

3) عدم وجود نوع السلامة


1 و 2 لحظات غير سارة ، ولكن يمكنك تحملها ، ولكن مع عدم وجود سلامة من النوع عند التسجيل ، من الصعب بالفعل تحملها ، لا ينبغي تجميع هذا:


 // Name - string // Age - int ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Name) 

نريد تلقي معلومات حول هذه الأخطاء في مرحلة الترجمة ، وليس في وقت التشغيل.


باستخدام مغلفة بسط للقضاء على هذه النقاط.


كتابة غلاف


لماذا يجب كتابة التسجيل بهذه الطريقة؟


 CreateMap<Pupil, PupilDto>() .ForMember(x => x.Name, s => s.MapFrom(x => x.Identity.Passport.Name)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Surname)) .ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.House, s => s.MapFrom(x => x.Address.House)) .ForMember(x => x.Street, s => s.MapFrom(x => x.Address.Street)) .ForMember(x => x.Country, s => s.MapFrom(x => x.Address.Country)) .ForMember(x => x.Surname, s => s.MapFrom(x => x.Identity.Passport.Age)) .ForMember(x => x.Group, s => s.MapFrom(x=>x.EducationCard.StudyGroup.Number)) 

موجزة أكثر بكثير:


 CreateMap<Pupil,PupilDto>() //    // PassportName = Passport.Name, PassportSurname = Passport.Surname .From(x => x.IdentityCard.Passport).To() // House,Street,Country -   .From(x => x.Address).To() //    -  DTO,  -  .From(x => x.EducationCard.Group).To((x => x.Group,x => x.Number)); 

تقبل الطريقة To الصفوف إذا كنت بحاجة إلى تحديد قواعد التعيين


IMapping <TSource، TDest> هي واجهة automaper التي يتم فيها تحديد أساليب ForMember، ForAll () ... كل هذه الطرق تُرجع هذا (Fluent Api).


سنعود المجمع لتذكر التعبير من الطريقة


 public static MapperExpressionWrapper<TSource, TDest, TProjection> From<TSource, TDest, TProjection> (this IMappingExpression<TSource, TDest> mapping, Expression<Func<TSource, TProjection>> expression) => new MapperExpressionWrapper<TSource, TDest, TProjection>(mapping, expression); 

الآن ، بعد كتابة الأسلوب From ، سيشاهد مبرمج التحميل الزائد للطريقة To ، وبالتالي سنخبره API ، في مثل هذه الحالات يمكننا أن ندرك كل سحر طرق التمديد ، وسعنا من توسيع نطاق السلوك دون الحاجة إلى الكتابة إلى مصادر أداة الإيقاف التلقائي


نحن اكتب


تطبيق الأسلوب المكتوب إلى أكثر تعقيدًا.


دعونا نحاول تصميم هذه الطريقة ، نحتاج إلى تقسيمها إلى أجزاء قدر الإمكان وإخراج كل المنطق بطرق أخرى. توافق فورًا على أننا سنقصر عدد معلمات tuple على عشرة.


عندما تحدث مشكلة مماثلة في ممارستي ، فإنني أنظر فورًا في اتجاه Roslyn ، ولا أشعر بأنني أكتب الكثير من نفس النوع من الأساليب وأقوم باستخدام "نسخ اللصق" ، فمن الأسهل إنشاءها.


في هذا العام سوف يساعدنا. من الضروري إنشاء 10 طرق مع عدد مختلف من الأدوية العامة والمعلمات


الطريقة الأولى للقذيفة كانت مختلفة بعض الشيء ، أردت تحديد أنواع الإرجاع من اللمدا (int ، string ، منطقية ، DateTime) وعدم استخدام أنواع عالمية.


تكمن الصعوبة في أنه حتى بالنسبة إلى 3 معلمات ، سيتعين علينا توليد 64 حمولة زائدة مختلفة ، وعند استخدام عام واحد فقط:


 IMappingExpression<TSource, TDest> To<TSource, TDest, TProjection,T,T1, T2, T3>( this MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0, (Expression<Func<TDest, T1>>, Expression<Func<TProjection, T1>>) arg1, (Expression<Func<TDest, T2>>, Expression<Func<TProjection, T2>>) arg2, (Expression<Func<TDest, T3>>, Expression<Func<TProjection, T3>>) arg3) { ... } 

ولكن ليست هذه هي المشكلة الرئيسية ، فنحن نقوم بإنشاء الشفرة ، وسوف يستغرق الأمر بعض الوقت وسنحصل على مجموعة كاملة من الطرق اللازمة.


المشكلة مختلفة ، لن تلتقط ReSharper الكثير من الأحمال الزائدة وترفض العمل فقط ، ستفقد Intellisience وتحميل IDE.


نحن نطبق طريقة تأخذ tuple واحد:


 public static IMappingExpression<TSource, TDest> To <TSource, TDest, TProjection, T>(this MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0) { //    RegisterByConvention(mapperExpressionWrapper); //    expreession RegisterRule(mapperExpressionWrapper, arg0); //  IMappingExpression,     //   extension  return mapperExpressionWrapper.MappingExpression; } 

أولاً ، دعنا نتحقق من تحديد خصائص الخصائص التي يمكن العثور عليها عن طريق الاصطلاح ، وهذه طريقة بسيطة إلى حد ما ، لكل خاصية في DTO نبحث عن المسار في الكيان الأصلي. يجب أن يتم استدعاء الأساليب بشكل منعكس ، لأنك تحتاج إلى الحصول على لامدا مكتوب ، ونوعها يعتمد على prop.


من المستحيل تسجيل lambda من النوع Expression <Func <TSource ، object >> ، ثم يقوم AutoMapper بتعيين جميع خصائص DTO لكتابة كائن


 private static void RegisterByConvention<TSource, TDest, TProjection>( MapperExpressionWrapper<TSource, TDest, TProjection> mapperExpressionWrapper) { var properties = typeof(TDest).GetProperties().ToList(); properties.ForEach(prop => { // mapperExpressionWrapper.FromExpression = x=>x.Identity.Passport // prop.Name = Name // ruleByConvention Expression<Func<Pupil,string>> x=>x.Identity.Passport.Name var ruleByConvention = _cachedMethodInfo .GetMethod(nameof(HelpersMethod.GetRuleByConvention)) .MakeGenericMethod(typeof(TSource), typeof(TProjection), prop.PropertyType) .Invoke(null, new object[] {prop, mapperExpressionWrapper.FromExpression}); if (ruleByConvention == null) return; // mapperExpressionWrapper.MappingExpression.ForMember(prop.Name, s => s.MapFrom((dynamic) ruleByConvention)); }); } 

يتلقى RegisterRule تلميحًا يحدد قواعد التعيين ، ويجب أن يكون "متصلًا" به
FromExpression والتعبير مرت إلى tuple.


هذا سوف يساعدنا Expression.Invoke ، EF Core 2.0 لم يدعم ذلك ، بدأت الإصدارات الأحدث في الدعم. سيسمح لك بعمل "تكوين لامب":


 Expression<Func<Pupil,StudyGroup>> from = x=>x.EducationCard.StudyGroup; Expression<Func<StudyGroup,int>> @for = x=>x.Number; //invoke = x=>x.EducationCard.StudyGroup.Number; var composition = Expression.Lambda<Func<Pupil, string>>( Expression.Invoke(@for,from.Body),from.Parameters.First()) 

طريقة التسجيل :


 private static void RegisterRule<TSource, TDest, TProjection, T (MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) rule) { //rule = (x=>x.Group,x=>x.Number) var (from, @for) = rule; //      @for = (Expression<Func<TProjection, T>>) _interpolationReplacer.Visit(@for); //mapperExpressionWrapper.FromExpression = (x=>x.EducationCard.StudyGroup) var result = Expression.Lambda<Func<TSource, T>>( Expression.Invoke(@for, mapperExpressionWrapper.FromExpression.Body), mapperExpressionWrapper.FromExpression.Parameters.First()); var destPropertyName = from.PropertiesStr().First(); // result = x => Invoke(x => x.Number, x.EducationCard.StudyGroup) //  ,  result = x=>x.EducationCard.StudyCard.Number mapperExpressionWrapper.MappingExpression .ForMember(destPropertyName, s => s.MapFrom(result)); } 

تم تصميم طريقة To لتكون سهلة عند إضافة معلمات tuple. عند إضافة مجموعة أخرى إلى المعلمات ، تحتاج إلى إضافة معلمة عامة أخرى ، واستدعاء طريقة RegisterRule للمعلمة الجديدة.


مثال على معلمتين:


 IMappingExpression<TSource, TDest> To<TSource, TDest, TProjection, T, T1> (this MapperExpressionWrapper<TSource,TDest,TProjection>mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) arg0, (Expression<Func<TDest, T1>>, Expression<Func<TProjection, T1>>) arg1) { RegisterByConvention(mapperExpressionWrapper); RegisterRule(mapperExpressionWrapper, arg0); RegisterRule(mapperExpressionWrapper, arg1); return mapperExpressionWrapper.MappingExpression; } 

نحن نستخدم CSharpSyntaxRewriter ، هذا هو الزائر الذي يمشي من خلال عقد شجرة بناء الجملة. نأخذ كطريقة مع To مع وسيطة واحدة وإضافة معلمة عامة واستدعاء RegisterRule ؛


 public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax node) { //     To if (node.Identifier.Value.ToString() != "To") return base.VisitMethodDeclaration(node); // returnStatement = return mapperExpressionWrapper.MappingExpression; var returnStatement = node.Body.Statements.Last(); //beforeReturnStatements: //[RegisterByConvention(mapperExpressionWrapper), // RegisterRule(mapperExpressionWrapper, arg0)] var beforeReturnStatements = node.Body.Statements.SkipLast(1); //   RegisterRule  returStatement var newBody = SyntaxFactory.Block( beforeReturnStatements.Concat(ReWriteMethodInfo.Block.Statements) .Concat(new[] {returnStatement})); //     return node.Update( node.AttributeLists, node.Modifiers, node.ReturnType, node.ExplicitInterfaceSpecifier, node.Identifier, node.TypeParameterList.AddParameters (ReWriteMethodInfo.Generics.Parameters.ToArray()), node.ParameterList.AddParameters (ReWriteMethodInfo.AddedParameters.Parameters.ToArray()), node.ConstraintClauses, newBody, node.SemicolonToken); } 

يحتوي ReWriteMethodInfo على العقد شجرة الإنشاء بناء الجملة التي تحتاج إلى إضافة. بعد ذلك ، نحصل على قائمة من 10 كائنات من النوع MethodDeclarationSyntax (شجرة بناء جملة تمثل طريقة).


في الخطوة التالية ، نأخذ الفصل الذي يكمن فيه القالب إلى الأسلوب وكتابة جميع الطرق الجديدة فيه باستخدام زائر آخر ، حيث نعيد تعريف VisitClassDeclatation.


تسمح لك طريقة التحديث بتحرير عقدة شجرة موجودة ، وتحت غطاء الشفرة يتكرر على جميع الوسائط التي تم تمريرها ، وإذا كان هناك اختلاف واحد على الأقل عن العقدة الأصلية ، فإنه ينشئ عقدة جديدة.


 public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { //todo refactoring it return node.Update( node.AttributeLists, node.Modifiers, node.Keyword, node.Identifier, node.TypeParameterList, node.BaseList, node.ConstraintClauses, node.OpenBraceToken, new SyntaxList<MemberDeclarationSyntax>(ReWriteMethods), node.CloseBraceToken, node.SemicolonToken); } 

في النهاية ، نحصل على SyntaxNode - فئة مع أساليب إضافية ، اكتب العقدة إلى ملف جديد ، والآن قمنا بتحميل over to method ، والتي تأخذ من 1 إلى 10 tuples وتعيين أكثر إيجازًا.


نقطة التوسع


دعونا ننظر في AutoMapper كشيء أكثر. يتعذر على موفر Queryable تحليل الكثير من الاستعلامات ، ويمكن إعادة كتابة جزء معين من هذه الاستعلامات بشكل مختلف. هذا هو المكان الذي يبدأ فيه تطبيق AutoMapper ، ويمثل الامتداد نقطة امتداد حيث يمكننا إضافة قواعدنا الخاصة.


سوف نستخدم الزائر من المقال السابق الذي يحل محل الاستيفاء في السلسلة بالتسلسل في طريقة RegusterRule ، ونتيجة لذلك ، فإن كل تعبيرات تحدد التعيين من الكيان ستخضع لهذا الزائر ، مما يلغي الحاجة للاتصال بـ ReWrite في كل مرة. لإدارة هو الإسقاط ، لكنه لا يزال يجعل الحياة أسهل.


يمكننا أيضًا إضافة بعض الامتدادات المريحة ، على سبيل المثال ، للتعيين حسب الحالة:


 CreateMap<Passport,PassportDto>() .ToIf(x => x.Age, x => x < 18, x => $"{x.Age}", x => "Adult") 

الشيء الرئيسي هو عدم اللعب مع هذا وعدم البدء في نقل المنطق المعقد إلى مستوى العرض
جيثب

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


All Articles