AutoMapper is one of the main tools used in the development of Enterprise applications, so I want to write as little code as possible when defining entity mapping.
I do not like duplication in MapFrom with wide projections.
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))
I would like to rewrite it like this:
CreateMap<Pupil, PupilDto>() .From(x=>x.IdentityCard.Passport).To()
AutoMapper can build mapping in memory as well as translate in SQL, it adds Expression, making a projection in the DTO according to the rules that you describe in the profiles.
EntityQueryable.Select(dtoPupil => new PupilDto() { Name = dtoPupil.Identity.Passport, Surname = dtoPupil.Identity.Passport.Surname})
80% percent of the mapping that I have to write is a mapping that completes the Expression from IQueryble.
It is very convenient:
public ActionResult<IEnumerable<PupilDto>> GetAdultPupils(){ var result = _context.Pupils .Where(x=>x.Identity.Passport.Age >= 18 && ...) .ProjectTo<PupilDto>().ToList(); return result; }
In the declarative style, we formed a request to the Pupils table, added filtering, projected into the necessary DTO and returned to the client, so you can write all the read methods of a simple CRUD interface. And all this will be done at the database level.
True, in serious applications, such actions are unlikely to satisfy customers.
1) It is very verbose, with "wide" mapping you have to write rules that do not fit on one line of code.
Profiles grow and turn into archives of code, which is written once and changed only by refactoring names.
2) If you use the mapping under the convention, the brevity of the name is lost
properties in DTO:
public class PupilDto { // Pupil IdentityCard // IdentityCard Passport public string IdentityCardPassportName { get; set; } public string IdentityCardPassportSurname { get; set; } }
3) Lack of type safety
1 and 2 - unpleasant moments, but you can put up with them, but it’s already difficult to accept the lack of type safety during registration, it should not be compiled:
// Name - string // Age - int ForMember(x => x.Age, s => s.MapFrom(x => x.Identity.Passport.Name)
We want to receive information about such errors at the compilation stage, and not at run-time.
With the help of extention wrappers eliminate these points.
Why should registration be written in this way?
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 much more concise:
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));
The To method will accept tuples if you need to specify mapping rules.
IMapping <TSource, TDest> is an automaper interface that defines the ForMember, ForAll () methods ... all of these methods return a return this (Fluent Api).
We will return the wrapper to remember the expression from the From method.
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);
Now the programmer, having written the method From, will immediately see the overload of the method To , thus we will give him an API, in such cases we can realize all the pleasures of the extension methods, we have expanded the behavior without having write access to the source code
The implementation of the typed method To is more complicated.
Let's try to design this method, we need to break it into parts as much as possible and bring all the logic to other methods. We will immediately agree that we will limit the number of tuple parameters to ten.
When I encounter a similar task in my practice, I immediately look towards Roslyn, I don’t want to write many methods of the same type and do Copy Paste, they are easier to generate.
This will help us generic'i. You need to generate 10 methods with different numbers of generics and parameters.
The first approach to the projectile was a bit different, I wanted to limit the returned lambda types (int, string, boolean, DateTime) and not use universal types.
The difficulty is that even for 3 parameters we will have to generate 64 different overloads, and when using generic only 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) { ... }
But this is not the main problem, we generate the code, it will take some time and we will get the whole set of necessary methods.
The problem is different, ReSharper will not pick up so many overloads and just refuse to work, you will lose Intellisience and load the IDE.
We implement a method that accepts one 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; }
First, we will check for which properties the convention mapping can be found, this is a fairly simple method, for each property in the DTO we are looking for a path in the original entity. Methods will have to be called reflexively, because you need to get a typed lambda, and its type depends on prop.
You cannot register lambda of type Expression <Func <TSource, object >>, then AutoMapper will match all properties of DTO to type object
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 gets a tuple that sets mapping rules, you need to "connect" in it
FromExpression and expression passed to the tuple.
Expression.Invoke will help us with this, EF Core 2.0 did not support it, later versions began to support. He will make a "lambda composition":
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 method:
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)); }
The To method is designed to be easy to expand when adding tuple parameters. When adding another tuple to the parameters, you need to add another generic parameter, and the RegisterRule method call for the new parameter.
An example for two parameters:
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; }
We use CSharpSyntaxRewriter , this is a visitor that goes through the nodes of the syntax tree. We take as a basis the method with a To with one argument and add a generic parameter and a RegisterRule call during the traversal process;
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); }
In ReWriteMethodInfo are the generated syntax tree nodes that need to be added. After this, we will get a list consisting of 10 objects with the MethodDeclarationSyntax type (the syntax tree representing the method).
In the next step, we take a class in which the To template method lies and write all new methods into it using another Visitor, in which we will redefine VisitClassDeclatation.
The Update method allows you to edit an existing tree node, it goes through all the passed arguments under the hood, and if at least one differs from the original one, it creates a new node.
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); }
In the end, we will get the SyntaxNode class with the added methods, we will write the node to a new file. Now we have To method overloads that accept from 1 to 10 tuples and a much more concise mapping.
Let's look at AutoMapper as something more. Queryable Provider cannot parse enough queries, and you can rewrite a certain part of these queries differently. This is where AutoMapper comes into play, extension'y is the extension point where we can add our own rules.
Apply the visitor from the previous article replacing string interpolation with concatenation in the RegusterRule method. As a result, all expressions that define mapping from an entity pass through this visitor, thus we will not have to call ReWrite each time. This is not a panacea, the only thing we can do control is a projection, but it still makes life easier.
We can also add some convenient extensions, for example, for mapping by condition:
CreateMap<Passport,PassportDto>() .ToIf(x => x.Age, x => x < 18, x => $"{x.Age}", x => "Adult")
The main thing is not to play around with it and not to start transferring complex logic to the display level.
Github
Source: https://habr.com/ru/post/444934/
All Articles