我们为自己调整AutoMapper

AutoMapper是开发企业应用程序时使用的主要工具之一,因此我想在定义实体映射时编写尽可能少的代码。


我不喜欢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,它可以根据您在配置文件中描述的规则完成Expression,并在DTO中进行投影。


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

我必须编写的映射的80%是完成IQueryble中Expression的映射。


这很方便:


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

以声明式的方式,我们在Pupils表上形成了一个查询,添加了过滤,投影到所需的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>是自动映射器的接口,在其中定义了ForMember,ForAll()方法...所有这些方法均返回此值(Fluent Api)。


我们将返回包装器以记住From方法中的Expression


 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,在这种情况下,我们可以实现扩展方法的所有魅力,我们可以扩展行为而无需访问自动映射器源


我们代表


实现类型化的To方法更加复杂。


让我们尝试设计这种方法,我们需要尽可能地将其分解成其他方法中的所有逻辑。 立即同意我们将元组参数的数量限制为十个。


当我在练习中遇到类似问题时,我立即朝Roslyn的方向看,我感觉自己并不想编写很多相同类型的方法并执行“复制粘贴”,因此生成它们更容易。


在此通用将对我们有所帮助。 有必要生成10个具有不同数量的泛型和参数的方法


射弹的第一种方法有些不同,我想限制lambda的返回类型(int,string,boolean,DateTime),而不要使用通用类型。


困难在于,即使对于3个参数,我们也必须生成64个不同的重载,而仅使用泛型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) { ... } 

但这不是主要问题,我们生成代码,这将花费一些时间,并且我们将获得全部必要的方法。


问题是不同的,ReSharper不会承担这么多的重载而只是拒绝工作,您将失去智能并加载IDE。


我们实现了一个采用一个元组的方法:


 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中的每个属性,我们都在原始实体中寻找路径。 方法将必须自反地调用,因为您需要获取类型化的lambda,并且其类型取决于prop。


无法注册类型为Expression <Func <TSource,object >>的lambda,然后AutoMapper将所有DTO属性映射到类型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接收一个定义了映射规则的元组,它需要在其中“连接”
FromExpression和表达式传递给元组。


这将帮助我们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()) 

RegisterRule方法:


 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方法被设计为在添加元组参数时易于扩展。 在向参数添加另一个元组时,您需要添加另一个泛型参数,并为新参数调用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(表示方法的语法树)的对象的列表。


在下一步中,我们将使用模板方法To所在的类,并使用另一个Visitor将所有新方法写入其中,在其中我们重新定义VisitClassDeclatation。


Update方法允许您编辑现有的树节点,在幕后遍历所有传递的参数,如果至少一个与原始参数不同,则创建一个新节点。


 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-一个带有添加方法的类,将该节点写入一个新文件中,现在我们重载了To方法,该方法需要1到10个元组以及更简洁的映射。


扩展点


让我们将AutoMapper视为更多内容。 Queryable Provider无法解析很多查询,并且这些查询的某些部分可以用不同的方式重写。 这是AutoMapper发挥作用的地方,扩展是我们可以添加自己的规则的扩展点。


我们将使用上一篇文章中的访问者,该字符串在RegusterRule方法中将字符串插值替换为串联,因此,所有定义来自该实体的映射的表达式都将通过该访问者,从而消除了每次调用ReWrite的需要。管理只是一个预测,但仍然可以使生活更轻松。


我们还可以添加一些方便的扩展,例如,按条件映射:


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

最主要的是不要玩这个游戏,也不要开始将复杂的逻辑转移到显示级别
Github

Source: https://habr.com/ru/post/zh-CN444934/


All Articles