Wir passen AutoMapper für uns an

AutoMapper ist eines der Hauptwerkzeuge für die Entwicklung von Unternehmensanwendungen. Daher möchte ich beim Definieren der Entitätszuordnung so wenig Code wie möglich schreiben.


Ich mag keine Vervielfältigung in MapFrom mit breiten Projektionen.


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

Ich würde es gerne so umschreiben:


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

Projectto


AutoMapper kann Mapping sowohl im Speicher erstellen als auch in SQL übersetzen. Es vervollständigt Expression und projiziert in DTO gemäß den Regeln, die Sie in den Profilen beschrieben haben.


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

80% der Zuordnung, die ich schreiben muss, ist die Zuordnung, die Expression from IQueryble vervollständigt.


Es ist sehr bequem:


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

In einem deklarativen Stil haben wir eine Abfrage für die Pupils-Tabelle erstellt, Filter hinzugefügt, in das gewünschte DTO projiziert und an den Client zurückgegeben, damit Sie alle Lesemethoden einer einfachen CRUD-Schnittstelle schreiben können. All dies wird auf Datenbankebene durchgeführt.


Es ist wahr, dass solche Aktionen bei ernsthaften Anwendungen die Kunden wahrscheinlich nicht zufrieden stellen.


Nachteile AutoMapper'a


1) Es ist sehr ausführlich, bei "breiter" Zuordnung müssen Sie Regeln schreiben, die nicht in eine Codezeile passen.


Profile wachsen und werden zu Archiven von Code, der einmal geschrieben wird und sich nur ändert, wenn Namen umgestaltet werden.


2) Wenn Sie die Zuordnung gemäß der Konvention verwenden, geht der Name verloren
Eigenschaften in DTO:


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

3) Mangelnde Typensicherheit


1 und 2 sind unangenehme Momente, aber Sie können sie ertragen, aber mit der mangelnden Typensicherheit bei der Registrierung ist es bereits schwieriger zu ertragen, dies sollte nicht kompiliert werden:


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

Wir möchten Informationen über solche Fehler in der Kompilierungsphase und nicht zur Laufzeit erhalten.


Verwenden von Erweiterungs-Wrappern, um diese Punkte zu entfernen.


Einen Wrapper schreiben


Warum sollte die Registrierung so geschrieben werden?


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

So viel prägnanter:


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

Die To- Methode akzeptiert Tupel, wenn Sie Zuordnungsregeln angeben müssen


IMapping <TSource, TDest> ist die Schnittstelle des Autoherstellers, in der die ForMember, ForAll () -Methoden definiert sind. Alle diese Methoden geben dies zurück (Fluent Api).


Wir werden den Wrapper zurückgeben, um uns an die Expression from From-Methode zu erinnern


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

Jetzt wird der Programmierer, der die From-Methode geschrieben hat, sofort die Überladung der To- Methode sehen, wodurch wir ihm die API mitteilen. In solchen Fällen können wir alle Reize der Erweiterungsmethoden erkennen. Wir haben das Verhalten erweitert, ohne Schreibzugriff auf die Autos zu haben


Wir typisieren


Das Implementieren einer typisierten To- Methode ist komplizierter.


Versuchen wir, diese Methode zu entwerfen. Wir müssen sie so weit wie möglich in Teile zerlegen und die gesamte Logik anderer Methoden herausnehmen. Stimmen Sie sofort zu, dass wir die Anzahl der Tupelparameter auf zehn beschränken.


Wenn in meiner Praxis eine ähnliche Aufgabe auftritt, schaue ich sofort in Richtung Roslyn. Ich habe keine Lust, viele der gleichen Methoden zu schreiben und Copy Paste auszuführen. Es ist einfacher, sie zu generieren.


In diesem Generikum wird uns helfen. Es ist erforderlich, 10 Methoden mit einer unterschiedlichen Anzahl von Generika und Parametern zu generieren


Die erste Annäherung an das Projektil war etwas anders. Ich wollte die Rückgabetypen von Lambdas (int, string, boolean, DateTime) einschränken und keine universellen Typen verwenden.


Die Schwierigkeit besteht darin, dass wir selbst für 3 Parameter 64 verschiedene Überladungen generieren müssen und bei Verwendung von generischen nur 1:


 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) { ... } 

Dies ist jedoch nicht das Hauptproblem. Wir generieren den Code, es wird einige Zeit dauern und wir werden alle erforderlichen Methoden erhalten.


Das Problem ist anders. ReSharper nimmt nicht so viele Überlastungen auf und weigert sich einfach zu arbeiten. Sie verlieren die Intelligenz und laden die IDE.


Wir implementieren eine Methode, die ein Tupel benötigt:


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

Lassen Sie uns zunächst prüfen, welche Eigenschaftenzuordnung gemäß Konvention gefunden werden kann. Dies ist eine ziemlich einfache Methode. Für jede Eigenschaft im DTO suchen wir nach dem Pfad in der ursprünglichen Entität. Methoden müssen reflexiv aufgerufen werden, da Sie ein typisiertes Lambda benötigen und dessen Typ von der Requisite abhängt.


Es ist unmöglich, ein Lambda vom Typ Ausdruck <Func <TSource, Objekt >> zu registrieren. Dann ordnet AutoMapper alle DTO-Eigenschaften dem Typ Objekt zu


 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 empfängt ein Tupel, das die Zuordnungsregeln definiert. Es muss darin "verbunden" sein
FromExpression und Ausdruck werden an das Tupel übergeben.


Dies wird uns helfen. Expression.Invoke, EF Core 2.0 unterstützte es nicht, spätere Versionen begannen zu unterstützen. Damit können Sie eine "Lambd-Komposition" erstellen:


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

RegisterRule- Methode:


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

Die To- Methode ist so konzipiert, dass sie beim Hinzufügen von Tupelparametern einfach erweitert werden kann. Wenn Sie den Parametern ein weiteres Tupel hinzufügen, müssen Sie einen weiteren generischen Parameter hinzufügen und die RegisterRule- Methode für den neuen Parameter aufrufen.


Ein Beispiel für zwei Parameter:


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

Wir verwenden CSharpSyntaxRewriter . Dies ist ein Besucher, der durch die Knoten des Syntaxbaums geht. Wir nehmen eine Methode mit To mit einem Argument als Basis und fügen einen generischen Parameter hinzu und rufen RegisterRule auf ;


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

Die ReWriteMethodInfo enthält die generierten Syntaxbaumknoten, die Sie hinzufügen müssen. Danach erhalten wir eine Liste von 10 Objekten vom Typ MethodDeclarationSyntax (ein Syntaxbaum, der eine Methode darstellt).


Im nächsten Schritt nehmen wir die Klasse, in der die Vorlagenmethode To liegt, und schreiben alle neuen Methoden mit einem anderen Besucher hinein, in dem wir VisitClassDeclatation neu definieren.


Mit der Update- Methode können Sie einen vorhandenen Baumknoten bearbeiten, unter der Haube alle übergebenen Argumente durchlaufen und, wenn sich mindestens eines vom ursprünglichen unterscheidet, einen neuen Knoten erstellen.


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

Am Ende erhalten wir einen SyntaxNode - eine Klasse mit hinzugefügten Methoden, schreiben den Knoten in eine neue Datei. Jetzt haben wir die To- Methode überladen, die 1 bis 10 Tupel und eine viel präzisere Zuordnung benötigt.


Expansionspunkt


Betrachten wir AutoMapper als etwas mehr. Der abfragbare Anbieter kann nicht viele Abfragen analysieren, und ein bestimmter Teil dieser Abfragen kann anders umgeschrieben werden. Hier kommt AutoMapper ins Spiel. Die Erweiterung ist ein Erweiterungspunkt, an dem wir unsere eigenen Regeln hinzufügen können.


Wir werden den Besucher aus dem vorherigen Artikel verwenden, der die Zeichenfolgeninterpolation durch Verkettung in der RegusterRule- Methode ersetzt. Infolgedessen werden alle Ausdrücke, die die Zuordnung von der Entität definieren, durch diesen Besucher geleitet, sodass nicht jedes Mal ReWrite aufgerufen werden muss. Dies ist kein Allheilmittel zu verwalten ist eine Projektion, aber es macht das Leben trotzdem einfacher.


Wir können auch eine bequeme Erweiterung hinzufügen, zum Beispiel für die Zuordnung nach Bedingung:


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

Die Hauptsache ist, nicht damit zu spielen und keine komplexe Logik auf die Anzeigeebene zu übertragen
Github

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


All Articles