AutoMapper 使用總結

初識AutoMapper

在開始本篇文章以前,先來思考一個問題:一個項目分多層架構,如顯示層、業務邏輯層、服務層、數據訪問層。層與層訪問須要數據載體,也就是類。若是多層通用一個類,一則會暴露出每層的字段,兩者會使類字段不少,並且會出現不少冗餘字段,這種方式是不可取的;若是每層都使用不一樣的類,則層與層調用時,一個字段一個字段的賦值又會很麻煩。針對第二種狀況,可使用AutoMapper來幫助咱們實現類字段的賦值及轉換。html

AutoMapper是一個對象映射器,它能夠將一個一種類型的對象轉換爲另外一種類型的對象。AutoMapper提供了映射規則及操做方法,使咱們不用過多配置就能夠映射兩個類。架構

安裝AutoMapper

經過Nuget安裝AutoMapper,本次使用版本爲6.2.2。app

AutoMapper配置

初始化

先建立兩個類用於映射:ui

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的映射。spa

  • 使用靜態方式
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);

完整的例子:orm

[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配置來實現映射關係。htm

建立自定義的Profile須要繼承Profile類:對象

public class MyProfile : Profile
{
	public MyProfile()
	{
		CreateMap<ProductEntity, ProductDTO>();
		// Other mapping configurations
	}
} 

完成例子:blog

[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

相關文章
相關標籤/搜索