寫在前面
AutoMapper目錄:
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(上)
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(中)
- 【AutoMapper官方文檔】DTO與Domin Model相互轉換(下)
- 未完待續。。。
本篇目錄:
- Mapping Inheritance-映射繼承
- Queryable Extensions (LINQ)-擴展查詢表達式
- Configuration-配置
- Conditional Mapping-條件映射
- 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 }
我們在前幾節點中說的自定義映射規則,其實也是屬于查詢表達式的一種,結合實體框架可以簡單的應用下,比如我們要映射到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>();
類型映射優先級
- Explicit Mapping (using .MapFrom())-顯式映射:優先級最高,我們使用MapFrom方法定義映射規則,比如:.ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.EventDate.Date))
- Inherited Explicit Mapping-繼承的顯式映射:就是存在繼承關系的MapFrom定義映射規則映射。
- Ignore Property Mapping-忽略屬性映射:使用Ignore方法指定屬性忽略映射,比如:Mapper.CreateMap<Source, Destination>().ForMember(dest => dest.SomeValuefff, opt => opt.Ignore());
- Convention Mapping (Properties that are matched via convention)-公約映射:公約映射即符合PascalCase命名規則的映射。如Source類中有Value屬性,Dest類中也有Value屬性,Source和Dest映射關系即是公約映射。
- 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
如果你覺得本篇文章對你有所幫助,請點擊右下部“推薦”,^_^
參考資料:
文章列表