• 【AutoMapper官方文档】DTO与Domin Model相互转换(下)


    写在前面

      AutoMapper目录:

      本篇目录:

      关于AutoMapper写到这基本的东西都差不多了,上一篇定义为灵活配置篇,本篇可以定义为扩展应用篇,加一些补充,关于AutoMapper的项目应用,网上找了几篇英文文章,虽然看不懂,但是代码是相通的,感觉很不错,主要是EntityFramework中运用AutoMapper,数据访问中使用AutoMapper,有支持的,也有反对的,也有提出建议的,自己也正在摸索,希望有机会写篇文章和大家分享下。

      插一句:写这些东西,看的人真的很少,还不如像前几天大家写篇水文,来讨论下C#的好坏增加点人气,呵呵,但是如果是这种思想来编程真是不可饶恕,写这种文章的目的不一定是分享给别人,也是对自己学习的另一种修炼,毕竟肚子没有什么东西,是写不出来,也是在逼迫自己去学习,当去学习一点东西后,发现其实并不像想象的那么简单,还有很多的东西要去学习,恨只恨自己晚生了几年,还需努力。

    Mapping Inheritance-映射继承

      关于映射继承,其实在“Lists and Array-集合和数组”这一节点有提到,但是只是说明下AutoMapper解决映射继承所使用的方式,这边我们说下关于AutoMapper在映射继承中的一些特性,比如下面转换示例:

    1         public class Order { }
    2         public class OnlineOrder : Order { }
    3         public class MailOrder : Order { }
    4 
    5         public class OrderDto { }
    6         public class OnlineOrderDto : OrderDto { }
    7         public class MailOrderDto : OrderDto { }

      源对象和目标对象存在继承关系,和“Lists and Array”节点里面的的示例一样,我们首先要配置AutoMapper,添加类型映射关系和依赖关系,如下:

    1             //配置 AutoMapper
    2             Mapper.CreateMap<Order, OrderDto>()
    3                   .Include<OnlineOrder, OnlineOrderDto>()
    4                   .Include<MailOrder, MailOrderDto>();
    5             Mapper.CreateMap<OnlineOrder, OnlineOrderDto>();
    6             Mapper.CreateMap<MailOrder, MailOrderDto>();

      关于这三段代码的意义,在“Lists and Array”节点中也有说明,如果我们注释掉第一段代码,我们在做派生类映射转换的时候就会报错,如果我们把下面两段代码去掉,我们在做派生类映射转换的时候就会映射到基类,说明第一段代码的意义是,不仅仅包含Order到OrderDto之间的类型映射,还包含Order与OrderDto所有派生类之间的映射,但是只是声明,如果要对派生类之间进行类型映射转换,就还得需要创建派生类之间的类型映射关系。

      我们“Lists and Array”节点中这样执行类型映射转换:

    1     var destinations = Mapper.Map<ParentSource[], ParentDestination[]>(sources);

      上面这段转换代码是指定了源泛型类型(Source)和目标泛型类型类型(Dest),所以AutoMapper会根据指定的类型就可以进行转换了,前提是类型映射关系配置正确,要不然就会报“AutoMapperConfigurationException”异常。如果我们不指定目标数据类型,然后就行转换会怎样呢?比如下面转换:

    1     var order = new OnlineOrder();
    2     var mapped = Mapper.Map(order, order.GetType(), typeof(OrderDto));
    3     Console.WriteLine("mapped Type:" + mapped.GetType());

      转换效果:

      代码中我们并没有指定目标数据类型,只是指定一个派生类的基类,如果按照我们的理解,这段代码执行的结果应该是:转换结果mapped对象的类型应该是OrderDto类型,但是确是我们希望想要的OnlineOrderDto类型,虽然我们没有指定目标类型为OnlineOrderDto,但是这一切AutoMapper都帮你做了,就是说AutoMapper会自动判断目标类型与源数据类型存在的关系,并找出最合适的派生类类型。

    Queryable Extensions (LINQ)-扩展查询表达式

      注:关于Entity Framework中数据类型映射正在研究,网上找了很多英文文章,还在消化中,这一节点只是简单的说下AutoMapper查询表达式的用法,过几天再整理一篇关于实体框架中运用数据类型映射的文章,功力不够,还请包涵。

      当我们使用Entity Framework与AutoMapper结合进行查询对象转换的时候,使用Mapper.Map方法,就会发现查询结果对象中的所有属性和目标属性对象属性都会转换,当然你也可以在查询结果集中构建一个所需结果的示例,但是这样做并不是可取的,AutoMapper的作者扩展了QueryableExtensions,使得我们在查询的时候就可以实现转换,比如下面示例:

     1         public class OrderLine
     2         {
     3             public int Id { get; set; }
     4             public int OrderId { get; set; }
     5             public Item Item { get; set; }
     6             public decimal Quantity { get; set; }
     7         }
     8         public class Item
     9         {
    10             public int Id { get; set; }
    11             public string Name { get; set; }
    12         }
    13 
    14         public class OrderLineDTO
    15         {
    16             public int Id { get; set; }
    17             public int OrderId { get; set; }
    18             public string Item { get; set; }
    19             public decimal Quantity { get; set; }
    20         }
    21 
    22         public List<OrderLineDTO> GetLinesForOrder(int orderId)
    23         {
    24             Mapper.CreateMap<OrderLine, OrderLineDTO>()
    25               .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name));
    26 
    27             using (var context = new orderEntities())
    28             {
    29                 return context.OrderLines.Where(ol => ol.OrderId == orderId)
    30                          .Project().To<OrderLineDTO>().ToList();
    31             }
    32         }

      代码中的.Project().To就是扩展的查询表达式,详细表达式代码:

      1 using System;
      2 using System.Collections.Generic;
      3 using System.Linq;
      4 using System.Linq.Expressions;
      5 using System.Reflection;
      6 using System.Text.RegularExpressions;
      7 
      8 namespace DTO_AutoMapper使用详解
      9 {
     10     public static class QueryableExtensions
     11     {
     12         public static ProjectionExpression<TSource> Project<TSource>(this IQueryable<TSource> source)
     13         {
     14             return new ProjectionExpression<TSource>(source);
     15         }
     16     }
     17 
     18     public class ProjectionExpression<TSource>
     19     {
     20         private static readonly Dictionary<string, Expression> ExpressionCache = new Dictionary<string, Expression>();
     21 
     22         private readonly IQueryable<TSource> _source;
     23 
     24         public ProjectionExpression(IQueryable<TSource> source)
     25         {
     26             _source = source;
     27         }
     28 
     29         public IQueryable<TDest> To<TDest>()
     30         {
     31              var queryExpression = GetCachedExpression<TDest>() ?? BuildExpression<TDest>();
     32 
     33             return _source.Select(queryExpression);
     34         }        
     35 
     36         private static Expression<Func<TSource, TDest>> GetCachedExpression<TDest>()
     37         {
     38             var key = GetCacheKey<TDest>();
     39 
     40             return ExpressionCache.ContainsKey(key) ? ExpressionCache[key] as Expression<Func<TSource, TDest>> : null;
     41         }
     42 
     43         private static Expression<Func<TSource, TDest>> BuildExpression<TDest>()
     44         {
     45             var sourceProperties = typeof(TSource).GetProperties();
     46             var destinationProperties = typeof(TDest).GetProperties().Where(dest => dest.CanWrite);
     47             var parameterExpression = Expression.Parameter(typeof(TSource), "src");
     48             
     49             var bindings = destinationProperties
     50                                 .Select(destinationProperty => BuildBinding(parameterExpression, destinationProperty, sourceProperties))
     51                                 .Where(binding => binding != null);
     52 
     53             var expression = Expression.Lambda<Func<TSource, TDest>>(Expression.MemberInit(Expression.New(typeof(TDest)), bindings), parameterExpression);
     54 
     55             var key = GetCacheKey<TDest>();
     56 
     57             ExpressionCache.Add(key, expression);
     58 
     59             return expression;
     60         }        
     61 
     62         private static MemberAssignment BuildBinding(Expression parameterExpression, MemberInfo destinationProperty, IEnumerable<PropertyInfo> sourceProperties)
     63         {
     64             var sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == destinationProperty.Name);
     65 
     66             if (sourceProperty != null)
     67             {
     68                 return Expression.Bind(destinationProperty, Expression.Property(parameterExpression, sourceProperty));
     69             }
     70 
     71             var propertyNames = SplitCamelCase(destinationProperty.Name);
     72 
     73             if (propertyNames.Length == 2)
     74             {
     75                 sourceProperty = sourceProperties.FirstOrDefault(src => src.Name == propertyNames[0]);
     76 
     77                 if (sourceProperty != null)
     78                 {
     79                     var sourceChildProperty = sourceProperty.PropertyType.GetProperties().FirstOrDefault(src => src.Name == propertyNames[1]);
     80 
     81                     if (sourceChildProperty != null)
     82                     {
     83                         return Expression.Bind(destinationProperty, Expression.Property(Expression.Property(parameterExpression, sourceProperty), sourceChildProperty));
     84                     }
     85                 }
     86             }
     87 
     88             return null;
     89         }
     90 
     91         private static string GetCacheKey<TDest>()
     92         {
     93             return string.Concat(typeof(TSource).FullName, typeof(TDest).FullName);
     94         }
     95 
     96         private static string[] SplitCamelCase(string input)
     97         {
     98             return Regex.Replace(input, "([A-Z])", " $1", RegexOptions.Compiled).Trim().Split(' ');
     99         }
    100     }    
    101 }
    View Code

      我们在前几节点中说的自定义映射规则,其实也是属于查询表达式的一种,结合实体框架可以简单的应用下,比如我们要映射到DTO中一个Count属性,来计算查询结果集中的数量,如下面代码:

    1 Mapper.CreateMap<Customer, CustomerDto>()
    2     .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName))
    3     .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count()));

      LINQ支持聚合查询,AutoMapper支持LINQ的扩展方法。在自定义映射中,如果我们讲属性名称TotalContacts改为ContactsCount,AutoMapper将自动匹配到COUNT()扩展方法和LINQ提供程序将转化计数到相关子查询汇总子记录。AutoMapper还可以支持复杂的聚合和嵌套的限制,如果LINQ提供的表达式支持它,例如下面代码:

    1 Mapper.CreateMap<Course, CourseModel>()
    2     .ForMember(m => m.EnrollmentsStartingWithA,
    3           opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count()));

      上面计算的是每门课程,学生名字开头为“A”的学生数量。

      不是所有的映射选项都支持表达式,因为它必须有LINQ的支持,支持的有:

    • MapFrom
    • Ignore

      不支持的有:

    • Condition
    • DoNotUseDestinationValue
    • SetMappingOrder
    • UseDestinationValue
    • UseValue
    • ResolveUsing
    • Any calculated property on your domain object

    Configuration-配置

    Profile-修饰

      AutoMapper提供了个性化设置Profile,使得我们转换后的数据格式可以多变,当然还可以配置全局格式等等,需要继承自Profile,并重写Configure方法,然后在AutoMapper初始化的时候,讲自定义配置添加到映射配置中,如下面示例:

     1         public class Order
     2         {
     3             public decimal Amount { get; set; }
     4         }
     5         public class OrderListViewModel
     6         {
     7             public string Amount { get; set; }
     8         }
     9         public class OrderEditViewModel
    10         {
    11             public string Amount { get; set; }
    12         }
    13         public class MoneyFormatter : ValueFormatter<decimal>
    14         {
    15             protected override string FormatValueCore(decimal value)
    16             {
    17                 return value.ToString("c");
    18             }
    19         }
    20         public class ViewModelProfile : Profile
    21         {
    22             protected override void Configure()
    23             {
    24                 CreateMap<Order, OrderListViewModel>();
    25                 ForSourceType<decimal>().AddFormatter<MoneyFormatter>();
    26             }
    27         }

      先创建了一个MoneyFormatter字符格式化类,然后创建ViewModelProfile配置类,在Configure方法中,添加类型映射关系,ForSourceType指的是讲元数据类型添加格式化,配置使用代码:

     1         public void Example()
     2         {
     3             var order = new Order { Amount = 50m };
     4             //配置 AutoMapper
     5             Mapper.Initialize(cfg =>
     6             {
     7                 cfg.AddProfile<ViewModelProfile>(); 
     8                 cfg.CreateMap<Order, OrderEditViewModel>();
     9             });
    10             //执行 mapping
    11             var listViewModel = Mapper.Map<Order, OrderListViewModel>(order);
    12             var editViewModel = Mapper.Map<Order, OrderEditViewModel>(order);
    13 
    14             Console.WriteLine("listViewModel.Amount:" + listViewModel.Amount);
    15             Console.WriteLine("editViewModel.Amount:" + editViewModel.Amount);
    16         }

      可以看到在Mapper.Initialize初始化的时候,把ViewModelProfile添加到AutoMapper配置中,泛型类型参数必须是Profile的派生类,因为我们在ViewModelProfile的Configure方法中添加了Order到OrderListViewModel类型映射关系,所以我们再初始化的时候就不需要添加了,转换效果:

    Naming Conventions-命名约定

      在“Flattening-复杂到简单”节点中,我们说到AutoMapper映射转换遵循PascalCase(帕斯卡命名规则),所以我们在类型名称命名要按照PascalCase进行命名,除了默认的命名规则,AutoMapper还提供了一种命名规则,如下:

    1 Mapper.Initialize(cfg => {
    2   cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
    3   cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention();
    4 });

      SourceMemberNamingConvention表示源数据类型命名规则,DestinationMemberNamingConvention表示目标数据类型命名规则,LowerUnderscoreNamingConvention和PascalCaseNamingConvention是AutoMapper提供的两个命名规则,前者命名是小写并包含下划线,后者就是帕斯卡命名规则,所以映射转换的效果是:property_name -> PropertyName。

      当然除了在AutoMapper初始化的时候配置命名规则,也可以在Profile中添加全局配置,如下:

    1 public class OrganizationProfile : Profile 
    2 {
    3   protected override void Configure() 
    4   {
    5     SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
    6     DestinationMemberNamingConvention = new PascalCaseNamingConvention();
    7   }
    8 }

    Conditional Mapping-条件映射

      AutoMapper允许在类型映射之前添加条件,例如下面示例:

     1         public class Foo
     2         {
     3             public int baz { get; set; }
     4         }
     5         public class Bar
     6         {
     7             public uint baz { get; set; }
     8         }
     9         public void Example()
    10         {
    11             var foo = new Foo { baz = 1 };
    12             //配置 AutoMapper
    13             Mapper.CreateMap<Foo, Bar>()
    14                     .ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0)));
    15             //执行 mapping
    16             var result = Mapper.Map<Foo, Bar>(foo);
    17 
    18             Console.WriteLine("result.baz:" + result.baz);
    19         }

      上面示例表示当源数据baz大于0的时候,才能执行映射,关键字是Condition,Condition方法接受一个Func<TSource, bool>类型参数,注意已经指定返回值为bool类型,方法签名:

    1         //
    2         // 摘要:
    3         //     Conditionally map this member
    4         //
    5         // 参数:
    6         //   condition:
    7         //     Condition to evaluate using the source object
    8         void Condition(Func<TSource, bool> condition);

      转换效果:

    AutoMapper版本变化点

      在AutoMapper1.1版本中,如果我们要对类型嵌套映射中加入自定义类型映射,比如下面示例:

    1     Mapper.CreateMap<Order, OrderDto>()
    2           .Include<OnlineOrder, OnlineOrderDto>()
    3           .Include<MailOrder, MailOrderDto>()
    4           .ForMember(o=>o.Id, m=>m.MapFrom(s=>s.OrderId));
    5     Mapper.CreateMap<OnlineOrder, OnlineOrderDto>()
    6           .ForMember(o=>o.Id, m=>m.MapFrom(s=>s.OrderId));
    7     Mapper.CreateMap<MailOrder, MailOrderDto>()
    8           .ForMember(o=>o.Id, m=>m.MapFrom(s=>s.OrderId));

      可以看出,我们需要在每个类型映射的地方要加:.ForMember(o=>o.Id, m=>m.MapFrom(s=>s.OrderId));但是Order、OnlineOrder和MailOrder存在继承关系,难道我们如果再加一个派生类映射,就得加一段这样代码,这样就会代码就会变得难以维护。在AutoMapper2.0版本中解决了这一问题,只需要下面这样配置就可以了:

    1     Mapper.CreateMap<Order, OrderDto>()
    2           .Include<OnlineOrder, OnlineOrderDto>()
    3           .Include<MailOrder, MailOrderDto>()
    4           .ForMember(o=>o.Id, m=>m.MapFrom(s=>s.OrderId));
    5     Mapper.CreateMap<OnlineOrder, OnlineOrderDto>();
    6     Mapper.CreateMap<MailOrder, MailOrderDto>();

    类型映射优先级

    1. Explicit Mapping (using .MapFrom())-显式映射:优先级最高,我们使用MapFrom方法定义映射规则,比如:.ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.EventDate.Date))
    2. Inherited Explicit Mapping-继承的显式映射:就是存在继承关系的MapFrom定义映射规则映射。
    3. Ignore Property Mapping-忽略属性映射:使用Ignore方法指定属性忽略映射,比如:Mapper.CreateMap<Source, Destination>().ForMember(dest => dest.SomeValuefff, opt => opt.Ignore());
    4. Convention Mapping (Properties that are matched via convention)-公约映射:公约映射即符合PascalCase命名规则的映射。如Source类中有Value属性,Dest类中也有Value属性,Source和Dest映射关系即是公约映射。
    5. Inherited Ignore Property Mapping-继承的忽略属性映射:优先级最低,就是存在继承关系的忽略属性映射。

      我们举个简单示例来说明下映射优先级:

     1         //Domain Objects
     2         public class Order { }
     3         public class OnlineOrder : Order
     4         {
     5             public string Referrer { get; set; }
     6         }
     7         public class MailOrder : Order { }
     8 
     9         //Dtos
    10         public class OrderDto
    11         {
    12             public string Referrer { get; set; }
    13         }
    14         public void Example2()
    15         {
    16             //配置 AutoMapper
    17             Mapper.CreateMap<Order, OrderDto>()
    18                   .Include<OnlineOrder, OrderDto>()
    19                   .Include<MailOrder, OrderDto>()
    20                   .ForMember(o => o.Referrer, m => m.Ignore());
    21             Mapper.CreateMap<OnlineOrder, OrderDto>();
    22             Mapper.CreateMap<MailOrder, OrderDto>();
    23 
    24             //执行 Mapping
    25             var order = new OnlineOrder { Referrer = "google" };
    26             var mapped = Mapper.Map(order, order.GetType(), typeof(OrderDto));
    27         }

      转换后mapped对象的Referrer属性值为“google”,但是你发现我们在配置映射规则的时候,不是把Referrer属性给Ignore(忽略)了吗?因为OnlineOrder的ReferrerOrderDto的Referrer属性符合PascalCase命名规则,即是公约映射,虽然忽略属性映射的优先级比公约映射高,但是上面示例中Order和OnlineOrder存在继承关系,即是继承的忽略属性映射,所以优先级比公约映射要低。

    后记

      示例代码下载:http://pan.baidu.com/s/1comgI

      如果你觉得本篇文章对你有所帮助,请点击右下部“推荐”,^_^

      参考资料:

  • 相关阅读:
    关于自动分裂的思考 | Solrex 杨文博的博客,记录我的生活、技术、思想和梦想
    在STL中,map按值来排序的实现方法_永不言弃是生命的基调!_百度空间
    C/C++学习路线(教材推荐)_Hello World!_百度空间
    Google C++ Style中允许使用的Boost库(1) 程序即人生 博客频道 CSDN.NET
    STL中map按值(value)排序
    程序即人生 » 移动平台现在可用的C++ 11特性
    开发者应该了解的 12 款 Eclipse 插件 编程语言 ITeye资讯
    Lisp的给力特性(V.S. Python3) 第二篇 程序即人生 博客频道 CSDN.NET
    Solidot | 地球上有多少Java程序员?
    在STL中,map按值来排序的实现方法
  • 原文地址:https://www.cnblogs.com/xishuai/p/3708483.html
Copyright © 2020-2023  润新知