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 {
3)缺乏类型安全性
1和2是不愉快的时刻,但是您可以忍受它们,但是由于注册时缺乏类型安全性,因此已经很难忍受,不应编译:
我们希望在编译阶段而不是在运行时接收有关此类错误的信息。
使用扩展包装器消除这些问题。
编写包装纸
为什么要用这种方式写注册?
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>()
如果您需要指定映射规则,则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) {
首先,让我们检查一下按惯例可以找到哪些属性映射,这是一种非常简单的方法,对于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 => {
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;
RegisterRule方法:
private static void RegisterRule<TSource, TDest, TProjection, T (MapperExpressionWrapper<TSource,TDest,TProjection> mapperExpressionWrapper, (Expression<Func<TDest, T>>, Expression<Func<TProjection, T>>) rule) {
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) {
ReWriteMethodInfo包含您需要添加的生成的语法树节点。 之后,我们获得了10个类型为MethodDeclarationSyntax(表示方法的语法树)的对象的列表。
在下一步中,我们将使用模板方法To所在的类,并使用另一个Visitor将所有新方法写入其中,在其中我们重新定义VisitClassDeclatation。
Update方法允许您编辑现有的树节点,在幕后遍历所有传递的参数,如果至少一个与原始参数不同,则创建一个新节点。
public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) {
最后,我们得到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