初識AutoMapper
在開始本篇文章之前,先來思考一個問題:一個項目分多層架構,如顯示層、業務邏輯層、服務層、數據訪問層。層與層訪問需要數據載體,也就是類。如果多層通用一個類,一則會暴露出每層的字段,二者會使類字段很多,而且會出現很多冗余字段,這種方式是不可取的;如果每層都使用不同的類,則層與層調用時,一個字段一個字段的賦值又會很麻煩。針對第二種情況,可以使用AutoMapper來幫助我們實現類字段的賦值及轉換。
AutoMapper是一個對象映射器,它可以將一個一種類型的對象轉換為另一種類型的對象。AutoMapper提供了映射規則及操作方法,使我們不用過多配置就可以映射兩個類。
安裝AutoMapper
通過Nuget安裝AutoMapper,本次使用版本為6.2.2。
AutoMapper配置
初始化
先創建兩個類用於映射:
public class ProductEntity { public string Name { get; set; } public decimal Amount { get; set; } } public class ProductDTO { public string Name { get; set; } public decimal Amount { get; set; } }
Automapper可以使用靜態類和實例方法來創建映射,下面分別使用這兩種方式來實現 ProductEntity -> ProductDTO的映射。
- 使用靜態方式
Mapper.Initialize(cfg => cfg.CreateMap<ProductEntity, ProductDTO>()); var productDTO = Mapper.Map<ProductDTO>(productEntity);
- 使用實例方法
MapperConfiguration configuration = new MapperConfiguration(cfg => cfg.CreateMap<ProductEntity, ProductDTO>()); var mapper = configuration.CreateMapper(); var productDTO = mapper.Map<ProductDTO>(productEntity);
完整的例子:
[TestMethod] public void TestInitialization() { var productEntity = new ProductEntity() { Name = "Product" + DateTime.Now.Ticks, Amount = 10 }; Mapper.Initialize(cfg => cfg.CreateMap<ProductEntity, ProductDTO>()); var productDTO = Mapper.Map<ProductDTO>(productEntity); Assert.IsNotNull(productDTO); Assert.IsNotNull(productDTO.Name); Assert.IsTrue(productDTO.Amount > 0); }
Profiles設置
除了使用以上兩總方式類配置映射關系,也可以使用Profie配置來實現映射關系。
創建自定義的Profile需要繼承Profile類:
public class MyProfile : Profile { public MyProfile() { CreateMap<ProductEntity, ProductDTO>(); // Other mapping configurations } }
完成例子:
[TestMethod] public void TestProfile() { var productEntity = new ProductEntity() { Name = "Product" + DateTime.Now.Ticks, Amount = 10 }; var configuration = new MapperConfiguration(cfg => cfg.AddProfile<MyProfile>()); var productDTO = configuration.CreateMapper().Map<ProductDTO>(productEntity); Assert.IsNotNull(productDTO); Assert.IsNotNull(productDTO.Name); Assert.IsTrue(productDTO.Amount > 0); }
除了使用AddProfile,也可以使用AddProfiles添加多個配置;同樣,可以同時使用Mapper和Profile,也可以添加多個配置:
var configuration = new MapperConfiguration(cfg => { cfg.AddProfile<MyProfile>(); cfg.CreateMap<ProductEntity, ProductDTO>(); });
扁平化映射
AutoMapper先映射名字一致的字段,如果沒有,則會嘗試使用以下規則來映射:
- 目標中字段去掉前綴“Get”后的部分
- 分割目標字段(根據Pascal命名方式)為單個單詞
先創建用到的映射類:
public class Product { public Supplier Supplier { get; set; } public string Name { get; set; } public decimal GetAmount() { return 10; } } public class Supplier { public string Name { get; set; } } public class ProductDTO { public string SupplierName { get; set; } public decimal Amount { get; set; } }
AutoMapper會自動實現Product.Supplier.Name -> ProductDTO.SupplierName, Product.GetTotal -> ProductDTO.Total的映射。
[TestMethod] public void TestFalttening() { var supplier = new Supplier() { Name = "Supplier" + DateTime.Now.Ticks }; var product = new Product() { Supplier = supplier, Name = "Product" + DateTime.Now.Ticks }; Mapper.Initialize(cfg => cfg.CreateMap<Product, ProductDTO>()); var productDTO = Mapper.Map<ProductDTO>(product); Assert.IsNotNull(productDTO); Assert.IsNotNull(productDTO.SupplierName); Assert.IsTrue(productDTO.Amount > 0); }
集合驗證
AutoMapper除了可以映射單個對象外,也可以映射集合對象。AutoMapper源集合類型支持以下幾種:
- IEnumerable
- IEnumerable<T>
- ICollection
- ICollection<T>
- IList
- IList<T>
- List<T>
- Arrays
簡單類型映射:
public class Source { public int Value { get; set; } } public class Destination { public int Value { get; set; } } [TestMethod] public void TestCollectionSimple() { Mapper.Initialize(cfg => cfg.CreateMap<Source, Destination>()); var sources = new[] { new Source {Value = 1}, new Source {Value = 2}, new Source {Value = 3} }; IEnumerable<Destination> ienumerableDest = Mapper.Map<Source[], IEnumerable<Destination>>(sources); ICollection<Destination> icollectionDest = Mapper.Map<Source[], ICollection<Destination>>(sources); IList<Destination> ilistDest = Mapper.Map<Source[], IList<Destination>>(sources); List<Destination> listDest = Mapper.Map<Source[], List<Destination>>(sources); Destination[] arrayDest = Mapper.Map<Source[], Destination[]>(sources); }
復雜對象映射:
public class Order { private IList<OrderLine> _lineItems = new List<OrderLine>(); public OrderLine[] LineItems { get { return _lineItems.ToArray(); } } public void AddLineItem(OrderLine orderLine) { _lineItems.Add(orderLine); } } public class OrderLine { public int Quantity { get; set; } } public class OrderDTO { public OrderLineDTO[] LineItems { get; set; } } public class OrderLineDTO { public int Quantity { get; set; } } [TestMethod] public void TestCollectionNested() { Mapper.Initialize(cfg => { cfg.CreateMap<Order, OrderDTO>(); cfg.CreateMap<OrderLine, OrderLineDTO>(); }); var order = new Order(); order.AddLineItem(new OrderLine {Quantity = 10}); order.AddLineItem(new OrderLine {Quantity = 20}); order.AddLineItem(new OrderLine {Quantity = 30}); var orderDTO = Mapper.Map<OrderDTO>(order); Assert.IsNotNull(orderDTO); Assert.IsNotNull(orderDTO.LineItems); Assert.IsTrue(orderDTO.LineItems.Length > 0); }
投影及條件映射
投影(指定字段)
除了以上使用的自動映射規則,AutoMapper還可以指定映射方式。下面使用ForMemeber指定字段的映射,將一個時間值拆分映射到日期、時、分:
public class Calendar { public DateTime CalendarDate { get; set; } public string Title { get; set; } } public class CalendarModel { public DateTime Date { get; set; } public int Hour { get; set; } public int Minute { get; set; } public string Title { get; set; } } [TestMethod] public void TestProjection() { var calendar = new Calendar() { Title = "2018年日歷", CalendarDate = new DateTime(2018, 1, 1, 11, 59, 59) }; Mapper.Initialize(cfg => cfg .CreateMap<Calendar, CalendarModel>() .ForMember(dest => dest.Date, opt => opt.MapFrom(src =>src.CalendarDate.Date)) .ForMember(dest => dest.Hour, opt => opt.MapFrom(src => src.CalendarDate.Hour)) .ForMember(dest => dest.Minute, opt => opt.MapFrom(src => src.CalendarDate.Minute))); var calendarModel = Mapper.Map<CalendarModel>(calendar); Assert.AreEqual(calendarModel.Date.Ticks, new DateTime(2018, 1, 1).Ticks); Assert.AreEqual(calendarModel.Hour, 11); Assert.AreEqual(calendarModel.Minute, 59); }
條件映射
有些情況下,我們會考慮添加映射條件,比如,某個值不符合條件時,不允許映射。針對這種情況可以使用ForMember中的Condition:
public class Source { public int Value { get; set; } } public class Destination { public uint Value { get; set; } } [TestMethod] public void TestConditionByCondition() { var source = new Source() { Value = 3 }; //如果Source.Value > 0, 則執行映射;否則,映射失敗 Mapper.Initialize(cfg => cfg .CreateMap<Source, Destination>() .ForMember(dest => dest.Value, opt => opt.Condition(src => src.Value > 0))); var destation = Mapper.Map<Destination>(source); //如果不符合條件,則拋出異常 Assert.IsTrue(destation.Value.Equals(3)); }
如果要映射的類符合一定的規則,而且有很多,針對每個類都創建一個CreaterMapper會很麻煩。可以使用AddConditionalObjectMapper指定對象映射規則,這樣就不用每個映射關系都添加一個CreateMapper。另外,也可以使用AddMemberConfiguration指定字段的映射規則,比如字段的前后綴:
public class Product { public string Name { get; set; } public int Count { get; set; } } public class ProductModel { public string NameModel { get; set; } public int CountMod { get; set; } } [TestMethod] public void TestConditionByConfiguration() { var product = new Product() { Name = "Product" + DateTime.Now.Ticks, Count = 10 }; var config = new MapperConfiguration(cfg => { //對象映射規則: 通過以下配置,可以映射所有”目標對象的名稱“等於“源對象名稱+Model”的類,而不用單個添加CreateMapper映射 cfg.AddConditionalObjectMapper().Where((s, d) => d.Name == s.Name + "Model"); //字段映射規則: 通過以下配置,可以映射“源字段”與“目標字段+Model或Mod”的字段 cfg.AddMemberConfiguration().AddName<PrePostfixName>(_ => _.AddStrings(p => p.DestinationPostfixes, "Model", "Mod")); }); var mapper = config.CreateMapper(); var productModel = mapper.Map<ProductModel>(product); Assert.IsTrue(productModel.CountMod == 10); }
需要注意的一點是,添加了以上配置,如果目標對象中有字段沒有映射到,則會拋出異常。
值轉換
如果配置了值轉換,AutoMapper會將修改轉換后的值以符合配置的規則。比如,配置目標對象中的值添加符號“@@”:
public class Source { public string Name { get; set; } } public class Destination { public string Name { get; set; } } [TestMethod] public void TestValueTransfer() { var source = new Source() { Name = "Bob" }; Mapper.Initialize(cfg => { cfg.CreateMap<Source, Destination>(); cfg.ValueTransformers.Add<string>(val => string.Format("@{0}@", val)); }); var destation = Mapper.Map<Destination>(source); Assert.AreEqual("@Bob@", destation.Name); }
空值替換
如果要映射的值為Null,則可以使用NullSubstitute指定Null值的替換值:
public class Source { public string Name { get; set; } } public class Destination { public string Name { get; set; } } [TestMethod] public void TestValueTransfer() { var source = new Source() { }; Mapper.Initialize(cfg => { cfg.CreateMap<Source, Destination>() .ForMember(dest => dest.Name, opt => opt.NullSubstitute("其他值")); }); var destation = Mapper.Map<Destination>(source); Assert.AreEqual("其他值", destation.Name); }
配置驗證及設置
配置了映射,但是如何確定是否映射成功或者是否有字段沒有映射呢?可以添加Mapper.AssertConfigurationIsValid();來驗證是否映射成功。默認情況下,目標對象中的字段都被映射到后,AssertConfigurationIsValid才會返回True。也就是說,源對象必須包含所有目標對象,這樣在大多數情況下不是我們想要的,我們可以使用下面的方法來指定驗證規則:
- 指定單個字段不驗證
- 指定整個Map驗證規則
public class Product { public string Name { get; set; } public int Amount { get; set; } } public class ProductModel { public string Name { get; set; } public int Amount { get; set; } public string ViewName { get; set; } } public class ProductDTO { public string Name { get; set; } public int Amount { get; set; } public string ViewName { get; set; } } [TestMethod] public void TestValidation() { var product = new Product() { Name = "Product" + DateTime.Now.Ticks, Amount = 10 }; Mapper.Initialize(cfg => { //1. 指定字段映射方式 cfg.CreateMap<Product, ProductModel>() .ForMember(dest => dest.ViewName, opt => opt.Ignore()); //如果不添加此設置,會拋出異常 //2. 指定整個對象映射方式 //MemberList: // Source: 檢查源對象所有字段映射成功 // Destination:檢查目標對象所有字段映射成功 // None: 跳過驗證 cfg.CreateMap<Product, ProductDTO>(MemberList.Source); }); var productModel = Mapper.Map<ProductModel>(product); var productDTO = Mapper.Map<ProductDTO>(product); //驗證映射是否成功 Mapper.AssertConfigurationIsValid(); }
設置轉換前后行為
有的時候你可能會在創建映射前后對數據做一些處理,AutoMapper就提供了這種方式:
public class Source { public string Name { get; set; } public int Value { get; set; } } public class Destination { public string Name { get; set; } public int Value { get; set; } } [TestMethod] public void TestBeforeOrAfter() { var source = new Source() { Name = "Product" + DateTime.Now.Ticks, }; Mapper.Initialize(cfg => { cfg.CreateMap<Source, Destination>() .BeforeMap((src, dest) => src.Value = src.Value + 10) .AfterMap((src, dest) => dest.Name = "Pobin"); }); var productModel = Mapper.Map<Destination>(source); Assert.AreEqual("Pobin", productModel.Name); }
反向映射
從6.1.0開始,AutoMapper通過調用Reverse可以實現反向映射。反向映射根據初始化時創建的正向映射規則來做反向映射:
public class Order { public decimal Total { get; set; } public Customer Customer { get; set; } } public class Customer { public string Name { get; set; } } public class OrderDTO { public decimal Total { get; set; } public string CustomerName { get; set; } } [TestMethod] public void TestReverseMapping() { var customer = new Customer { Name = "Tom" }; var order = new Order { Customer = customer, Total = 20 }; Mapper.Initialize(cfg => { cfg.CreateMap<Order, OrderDTO>() .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)) //正向映射規則 .ReverseMap(); //設置反向映射 }); //正向映射 var orderDTO = Mapper.Map<OrderDTO>(order); //反向映射:使用ReverseMap,不用再創建OrderDTO -> Order的映射,而且還能保留正向的映射規則 var orderConverted = Mapper.Map<Order>(orderDTO); Assert.IsNotNull(orderConverted.Customer); Assert.AreEqual("Tom", orderConverted.Customer.Name); }
如果反向映射中不想使用原先的映射規則,也可以取消掉:
Mapper.Initialize(cfg => { cfg.CreateMap<Order, OrderDTO>() .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)) //正向映射規則 .ReverseMap() .ForPath(src => src.Customer.Name, opt => opt.Ignore()); //設置反向映射 });
自定義轉換器
有些情況下目標字段類型和源字段類型不一致,可以通過類型轉換器實現映射,類型轉換器有三種實現方式:
void ConvertUsing(Func<TSource, TDestination> mappingFunction); void ConvertUsing(ITypeConverter<TSource, TDestination> converter); void ConvertUsing<TTypeConverter>() where TTypeConverter : ITypeConverter<TSource, TDestination>;
下面通過一個例子來演示下以上三種類型轉換器的使用方式:
namespace AutoMapperSummary { [TestClass] public class CustomerTypeConvert { public class Source { public string Value1 { get; set; } public string Value2 { get; set; } public string Value3 { get; set; } } public class Destination { public int Value1 { get; set; } public DateTime Value2 { get; set; } public Type Value3 { get; set; } } public class DateTimeTypeConverter : ITypeConverter<string, DateTime> { public DateTime Convert(string source, DateTime destination, ResolutionContext context) { return System.Convert.ToDateTime(source); } } public class TypeTypeConverter : ITypeConverter<string, Type> { public Type Convert(string source, Type destination, ResolutionContext context) { return Assembly.GetExecutingAssembly().GetType(source); } } [TestMethod] public void TestTypeConvert() { var config = new MapperConfiguration(cfg => { cfg.CreateMap<string, int>().ConvertUsing((string s) => Convert.ToInt32(s)); cfg.CreateMap<string, DateTime>().ConvertUsing(new DateTimeTypeConverter()); cfg.CreateMap<string, Type>().ConvertUsing<TypeTypeConverter>(); cfg.CreateMap<Source, Destination>(); }); config.AssertConfigurationIsValid(); //驗證映射是否成功 var source = new Source { Value1 = "20", Value2 = "2018/1/1", Value3 = "AutoMapperSummary.CustomerTypeConvert+Destination" }; var mapper = config.CreateMapper(); var destination = mapper.Map<Source, Destination>(source); Assert.AreEqual(typeof(Destination), destination.Value3); } } }
自定義解析器
使用AutoMapper的自帶解析規則,我們可以很方便的實現對象的映射。比如:源/目標字段名稱一致,“Get/get + 源字段“與"目標字段"一致等。除了這些簡單的映射,還可以使用ForMember指定字段映射。但是,某些情況下,解析規則會很復雜,使用自帶的解析規則無法實現。這時可以自定義解析規則,可以通過以下三種方式使用自定義的解析器:
ResolveUsing<TValueResolver> ResolveUsing(typeof(CustomValueResolver)) ResolveUsing(aValueResolverInstance)
下面通過一個例子來演示如何使用自定義解析器:
public class Source { public string FirstName { get; set; } public string LastName { get; set; } } public class Destination { public string Name { get; set; } } /// <summary> /// 自定義解析器: 組合姓名 /// </summary> public class CustomResolver : IValueResolver<Source, Destination, string> { public string Resolve(Source source, Destination destination, string destMember, ResolutionContext context) { if (source != null && !string.IsNullOrEmpty(source.FirstName) && !string.IsNullOrEmpty(source.LastName)) { return string.Format("{0} {1}", source.FirstName, source.LastName); } return string.Empty; } } [TestMethod] public void TestResolver() { Mapper.Initialize(cfg => cfg.CreateMap<Source, Destination>() .ForMember(dest => dest.Name, opt => opt.ResolveUsing<CustomResolver>())); Mapper.AssertConfigurationIsValid(); var source = new Source { FirstName = "Michael", LastName = "Jackson" }; var destination = Mapper.Map<Source, Destination>(source); Assert.AreEqual("Michael Jackson", destination.Name); }
AutoMapper封裝
AutoMapper功能很強大,自定義配置支持也非常好,但是真正項目中使用時卻很少用到這么多功能,而且一般都會對AutoMapper進一步封裝使用。一方面使用起來方面,另外一方面也可以使代碼統一。下面的只是做一個簡單的封裝,還需要結合實際項目使用:
/// <summary> /// AutoMapper幫助類 /// </summary> public class AutoMapperManager { private static readonly MapperConfigurationExpression MapperConfiguration = new MapperConfigurationExpression(); static AutoMapperManager() { } private AutoMapperManager() { AutoMapper.Mapper.Initialize(MapperConfiguration); } public static AutoMapperManager Instance { get; } = new AutoMapperManager(); /// <summary> /// 添加映射關系 /// </summary> /// <typeparam name="TSource"></typeparam> /// <typeparam name="TDestination"></typeparam> public void AddMap<TSource, TDestination>() where TSource : class, new() where TDestination : class, new() { MapperConfiguration.CreateMap<TSource, TDestination>(); } /// <summary> /// 獲取映射值 /// </summary> /// <typeparam name="TDestination"></typeparam> /// <param name="source"></param> /// <returns></returns> public TDestination Map<TDestination>(object source) where TDestination : class, new() { if (source == null) { return default(TDestination); } return Mapper.Map<TDestination>(source); } /// <summary> /// 獲取集合映射值 /// </summary> /// <typeparam name="TDestination"></typeparam> /// <param name="source"></param> /// <returns></returns> public IEnumerable<TDestination> Map<TDestination>(IEnumerable source) where TDestination : class, new() { if (source == null) { return default(IEnumerable<TDestination>); } return Mapper.Map<IEnumerable<TDestination>>(source); } /// <summary> /// 獲取映射值 /// </summary> /// <typeparam name="TSource"></typeparam> /// <typeparam name="TDestination"></typeparam> /// <param name="source"></param> /// <returns></returns> public TDestination Map<TSource, TDestination>(TSource source) where TSource : class, new () where TDestination : class, new() { if (source == null) { return default(TDestination); } return Mapper.Map<TSource, TDestination>(source); } /// <summary> /// 獲取集合映射值 /// </summary> /// <typeparam name="TSource"></typeparam> /// <typeparam name="TDestination"></typeparam> /// <param name="source"></param> /// <returns></returns> public IEnumerable<TDestination> Map<TSource, TDestination>(IEnumerable<TSource> source) where TSource : class, new() where TDestination : class, new() { if (source == null) { return default(IEnumerable<TDestination>); } return Mapper.Map<IEnumerable<TSource>, IEnumerable<TDestination>>(source); } /// <summary> /// 讀取DataReader內容 /// </summary> /// <typeparam name="TDestination"></typeparam> /// <param name="reader"></param> /// <returns></returns> public IEnumerable<TDestination> Map<TDestination>(IDataReader reader) { if (reader == null) { return new List<TDestination>(); } var result = Mapper.Map<IEnumerable<TDestination>>(reader); if (!reader.IsClosed) { reader.Close(); } return result; } }
總結
本篇文章列舉了AutoMapper的基本使用方式,更多的使用可以參考官方文檔:http://automapper.readthedocs.io/en/latest/index.html