AutoMapper初步学习
前言
在一个应用程序中,如果在两个不同的类型对象之间传输数据,通常我们会用DTOs(数据传输对象),View Models(视图模型),或者直接是一些从一个service或者Web API的一些请求或应答对象。一个常见的需要使用数据传输对象的情况是,我们想把属于一个对象的某些属性值赋值给另一个对象的某些属性值,但是问题是,这个两个对象可能并不是完全匹配的,比如,两者之间的属性类型,名称等等,是不一样的,或者我们只是想把一个对象的一部分属性值赋值给另一个对象。
手动映射
以往的方式,都是手动映射去一一对应需要的某些属性值,我们来看一下是如何处理的。
通过以下这个例子来直观感受这种方式,创建了以下三个类:
public class Author {
public string Name { get; set; }
}
public class Book {
public string Title { get; set; }
public Author Author { get; set; }
}
public class BookViewModel {
public string Title { get; set; }
public string Author { get; set; }
}
为了创建Book对象实例的一个View Model对象实例-BookViewModel对象实例,我们需要写如下代码:
BookViewModel model = new BookViewModel
{
Title = book.Title,
Author = book.Author.Name
}
我们可以看到在上面的代码中,如果一旦在Book对象里添加了一个额外的字段,而后想在前台页面输出这个字段,那么就需要去在项目里找到每一处有这样转换字段的地方,这是非常繁琐的。
另外,BookViewModel.Author是一个string类型的字段,但是Book.Author属性却是Author对象类型的,我们用的解决方法是通过Book.Auther对象来取得Author的Name属性值,然后再赋值给BookViewModel的Author属性,这样看起行的通,但是想一想,如果打算在以后的开发中把Name拆分成两个-FisrtName和LastName,那么,我们得去把原来的ViewModel对象也拆分成对应的两个字段,然后在项目中找到所有的转换,然后替换。
那么有什么办法或者工具来帮助我们能够避免这样的情况发生呢?AutoMapper正是符合要求的一款插件。
了解Automapper
一、什么是Automapper
AutoMapper是一个对象和对象间的映射器。对象与对象的映射是通过转变一种类型的输入对象为一种不同类型的输出对象工作的。让AutoMapper有意思的地方,在于它提供了一些将类型A映射到类型B这种无聊的事情的有趣惯例。只要类型B遵守AutoMapper已经建立的惯例,大多数情况下,映射两种类型零配置就可以了。
二、为啥用Automapper
映射代码是无聊的。测试映射代码更无聊。AutoMapper提供了一些简单配置,还有一些简单的映射测试。真正的问题可能是“为什么使用对象-对象的映射呢”?映射可能发生在一个应用的许多地方,但大多数情况下都发生在层与层之间的边界,比如UI/Domain层之间,或者Service/Domain层之间。关注一层通常和关注另一层发生冲突,因此对象-对象间的映射来隔离模型model,这样就只会影响每一层关注的类型。
三、开始用Automapper
向项目中添加AutoMapper的引用有两种方式:
1、Nuget方式
在需要使用AutoMapper的项目文件上面右键→管理Nuget程序包,打开Nuget界面,搜索Automapper,然后安装第一个即可。如下图:
2、程序包管理控制台方式
点击Visual Studio的工具菜单→程序包管理控制台,然后选择需要安装Automapper的项目(下图中的默认项目),最后在控制台里面输入命令“Install-Package AutoMapper”命令即可安装Automapper包:
安装好AutoMapper后,让我们开始看看该怎么在代码中使用它吧!
使用AutoMappe
一、最简单的用法
从上面看来,AutoMapper的安装非常的便捷,只需要从Nuget上下载AutoMapper的包到你的应用程序里,然后添加对AutoMapper命名空间的引用,然后就可以在的项目里使用它了。而AutoMapper使用起来还是比较简单的,最简单的用法你只需要两句话:
public class User {
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public class UserDto {
public string Name { get; set; }
public int Age { get; set; }
}
Mapper.Initialize(cfg => cfg.CreateMap<User, UserDto>());
// var config = new MapperConfiguration(cfg => {
// cfg.CreateMap<User, UserDto>();
// })
// var mapper = config.CreateMapper();
var model = Mapper.Map<UserDto>(user);
首先创建映射,然后传入需要映射的对象执行映射,肥肠简单。使用AutoMappeer的好处是显而易见的,它不再需要我们去对DTO实例的属性一一赋值,无论你在User对象或者UserDto对象里加了一个或者更多的字段,那都不会影响这个段映射的代码,不再需要去找到每一处转换的地方去更改代码,你的程序会像之前正常运转。
二、如果属性名称不同
例如,将UserDto的Name属性改成Name2
Mapper.Initialize(cfg =>
cfg.CreateMap<User, UserDto>()
.ForMember(dest =>dest.Name2, opt => {
opt.MapFrom(src => src.Name);
})
);
User user = new User()
{
Id = 1,
Name = "caoyc",
Age = 20
};
var dto = Mapper.Map<UserDto>(user);
三、使用Profile配置
自定义一个UserProfile类继承Profile,并重写Configure方法
public class UserProfile:Profile {
public override void Configure() {
CreateMap<User, UserDto>()
.ForMember(d => d.Name2, opt =>
{
opt.MapFrom(s => s.Name);
});
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user = new User()
{
Id = 1,
Name = "caoyc",
Age = 20
};
var dto = Mapper.Map<UserDto>(user);
四、空值替换NullSubstitute
空值替换允许我们将Source对象中的空值在转换为Destination的值的时候,使用指定的值来替换空值。
public class UserProfile:Profile {
public override void Configure() {
CreateMap<User, UserDto>()
.ForMember(d => d.Name2, opt =>
{
.ForMember(d => d.Name2, opt => opt.MapFrom(s => s.Name))
.ForMember(d => d.Name2, opt => opt.NullSubstitute("值为空"));
});
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user = new User()
{
Id = 1,
Age = 20
};
var dto = Mapper.Map<UserDto>(user);
五、忽略属性Ignore
public class UserProfile:Profile {
public override void Configure() {
CreateMap<User, UserDto>().ForMember("Name", opt => opt.Ignore());
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user = new User()
{
Id = 1,
Name="caoyc",
Age = 20
};
var dto = Mapper.Map<UserDto>(user);
六、预设值
如果目标属性多于源属性,可以进行预设值
public class User {
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
public class UserDto {
public string Name { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
}
public class UserProfile:Profile {
public override void Configure() {
CreateMap<User, UserDto>();
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user = new User()
{
Id = 1,
Name="caoyc",
Age = 20
};
UserDto dto = new UserDto() {Gender = "男"};
Mapper.Map(user, dto);
七、条件约束Condition
当满足条件时才进行映射字段,例如人类年龄,假设我们现在人类年龄范围为0-120岁(这只是假设),只有满足在这个条件才进行映射
public class User {
public int Age { get; set; }
}
public class UserDto {
public int Age { get; set; }
}
public class UserProfile:Profile {
public override void Configure() {
CreateMap<User, UserDto>().ForMember(dest=>dest.Age,opt=>opt.Condition(src=>src.Age>=0 && src.Age<=120));
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user0 = new User() { Age = 1 };
User user1 = new User() { Age = 120 };
User user2 = new User() { Age = 201 };
var dto0= Mapper.Map<UserDto>(user0);
var dto1 = Mapper.Map<UserDto>(user1);
var dto2 = Mapper.Map<UserDto>(user2);
dto0.Age.ShouldEqual(1);
dto1.Age.ShouldEqual(120);
dto2.Age.ShouldEqual(0);
更进一步
此章节里面,我写了一些更深入一点的使用方法,比如自定义类型转换器、自定义值解析器等。更多请参考官方文档。
一、展平
对象 - 对象映射的一个常见用法是采用复杂的对象模型并将其展平为更简单的模型。我们可以采用复杂的模型,例如:
public class Order {
private readonly IList<OrderLineItem> _orderLineItems = new List<OrderLineItem>();
public Customer Customer { get; set; }
public OrderLineItem[] GetOrderLineItems()
{
return _orderLineItems.ToArray();
}
public void AddOrderLineItem()
{
_orderLineItems.Add(new OrderLineItem(product, quantity));
}
public decimal GetTotal()
{
return _orderLineItems.Sum(li => li.GetTotal());
}
}
public class Customer {
public string Name { get; set; }
}
public class OrderLineItem {
public Customer Customer (Product product,int quantity)
{
Product = product;
Quantity = quantity;
}
public Product Product { get; private set; }
public int Quantity { get; private set; }
public decimal GetTotal()
{
return Quantity*Product.Price;
}
}
public class Product {
public decimal Price { get; set; }
public string Name { get; set; }
}
我们希望将这个复杂的Order对象展平为一个更简单的OrderDto,它只包含特定场景所需的数据:
public class OrderDto {
public string CustomerName { get; set; }
public decimal Total { get; set; }
}
在AutoMapper中配置源/目标类型对时,配置程序会尝试将源类型上的属性和方法与目标类型上的属性进行匹配。如果对于目标类型的任何属性,源类型上不存在前缀为“Get”的属性,方法或方法,则AutoMapper会将目标成员名称拆分为单个单词(通过PascalCase约定)。
var customer = new Customer
{
Name = "George Costanza"
};
var order = new Order
{
Customer = customer
};
var bosco = new Product
{
Name = "Bosco",
Price = 4.99
};
order.AddOrderLineItem(bosco, 15);
// Configure AutoMapper
Mapper.Initialize(cfg => cfg.CreateMap<Order, OrderDto>());
// Perform mapping
OrderDto dto = Mapper.Map<Order, OrderDto>(order);
dto.CustomerName.ShouldEqual("George Costanza");
dto.Total.ShouldEqual(74.85);
二、反向映射ReverseMap
从6.1.0开始,AutoMapper现在支持更丰富的反向映射支持。请看实体:
public class Order {
public decimal decimal { get; set; }
public Customer Customer { get; set; }
}
public class Customer {
public string Gender { get; set; }
}
我们可以把它变成DTO:
public class OrderDto {
public string CustomerName { get; set; }
public decimal Total { get; set; }
}
我们可以映射两个方向,通过调用ReverseMap,AutoMapper创建一个反向映射配置:
Mapper.Initialize(cfg => {
cfg.CreateMap<Order, OrderDto>().ReverseMap();
});
var customer = new Customer {
Name = "Bob"
};
var order = new Order {
Customer = customer,
Total = 15.8
};
var orderDto = Mapper.Map<Order, OrderDto>(order);
orderDto.CustomerName = "Joe";
Mapper.Map(orderDto, order);
order.Customer.Name.ShouldEqual("Joe");
三、类型转换ITypeConverter
我们上面看到的,都是类型一样的进行映射。如果类型不一样怎么办呢??
一起看下面的例子是如何做的吧!
如果User数据中Gender存储的int类型,而DTO中Gender是String类型
public class User {
public int Gender { get; set; }
}
public class UserDto {
public string Gender { get; set; }
}
此时,需要类型转换,类型转换类,需要实现接口ITypeConverter
public class GenderTypeConvertert:ITypeConverter<int, string> {
public string Convert(int source,string destination,ResolutionContext context ) {
switch (source)
{
case 0:destination = "男";break;
case 1:destination = "女";break;
default:destination = "不详";break;
}
return destination;
}
}
public class UserProfile:Profile {
public override void Configure() {
CreateMap<int, string>().ConvertUsing<GenderTypeConvertert>();
//CreateMap<int, string>().ConvertUsing(new GenderTypeConvertert());
CreateMap<User, UserDto>();
}
}
Mapper.Initialize(x => x.AddProfile<UserProfile>());
User user0 = new User() { Gender = 0 };
User user1 = new User() { Gender = 1 };
User user2 = new User() { Gender = 2 };
var dto0 = Mapper.Map<UserDto>(user0);
var dto1 = Mapper.Map<UserDto>(user1);
var dto2 = Mapper.Map<UserDto>(user2);
dto0.Age.ShouldEqual("男");
dto1.Age.ShouldEqual("女");
dto2.Age.ShouldEqual("不详");
四、值解析器IValueResolver
虽然AutoMapper涵盖了相当多的目标成员映射方案,但是有1到5%的目标值需要一些帮助才能解析。例如,我们可能希望在映射期间获得计算值:
public class Source {
public int Value1 { get; set; }
public int Value2 { get; set; }
}
public class Destination {
public int Total { get; set; }
}
现在,我们希望Total为源Value属性的总和。需要提供自定义值解析器,首先创建一个实现IValueResolver的类型:
public class CustomResolver :IValueResolver<Source, Destination, int> {
public int Resolve(Source source, Destination destination, int member, ResolutionContext context) {
return source.Value1 + source.Value2;
}
}
Mapper.Initialize(cfg =>
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Total, opt => opt.MapFrom<CustomResolver>()));
var source = new Source
{
Value1 = 5,
Value2 = 7
};
var result = Mapper.Map<Source, Destination>(source);
result.Total.ShouldEqual(12);
在web里面使用
拿MVC来举例,MVC项目的应用中,可以将Mapper.Initialize封装到一个类里
public static class AutoMapperForMvc
{
public static void Register()
{
Mapper.Initialize(x => {
x.AddProfile<SourceProfile>();
});
}
}
进而在MVC的Global中进一次性注册:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
//注册
AutoMapperForMvc.Register();
}
}