AutoMapper 使用總結


初識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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM