一、概要
1、DTO?
DTO(Data Transfer Object)就是数据传输对象,说白了就是一个对象,只不过里边全是数据而已。
为什么要用DTO?
(1)DTO更注重数据,对领域对象进行合理封装,从而不会将领域对象的行为过分暴露给表现层
(2)DTO是面向UI的需求而设计的,而领域模型是面向业务而设计的。因此DTO更适合于和表现层的交互,通过DTO我们实现了表现层与领域Model之间的解耦,因此改动领域Model不会影响UI层
(3)DTO说白了就是数据而已,不包含任何的业务逻辑,属于瘦身型的对象,使用时可以根据不同的UI需求进行灵活的运用
现在我们既然知道了使用DTO的好处,那么我们肯定也想马上使用它,但是这里会牵扯一个问题:怎样实现DTO和领域Model之间的转换? 有两个思路,我们要么自己写转换代码,要么使用工具。不过就应用而言,我还是觉得用工具比较简单快捷,那就使用工具吧。其实这样的转换工具很多,不过我还是决定使用AutoMapper,因为它足够轻量级,而且也非常流行,国外的大牛们都使用它。使用AutoMapper可以很方便的实现DTO和领域Model之间的转换,它是一个强大的Object-Object Mapping工具。
2、AutoMapper是什么?
AutoMapper是一个对象-对象映射器。对象-对象映射通过将一种类型的输入对象转换为另一种类型的输出对象来工作。使AutoMapper变得有趣的是,它提供了一些有趣的约定,以免去搞清楚如何将类型A映射为类型B。只要类型B遵循AutoMapper既定的约定,就需要几乎零配置来映射两个类型。
3、为什么使用AutoMapper?
“为什么使用对象-对象映射?” 映射可以在应用程序中的许多地方发生,但主要发生在层之间的边界中,例如,UI /域层之间或服务/域层之间。一层的关注点通常与另一层的关注点冲突,因此对象-对象映射导致分离的模型,其中每一层的关注点仅会影响该层中的类型。
二、AutoMapper简单实例
首先,在项目中引用NuGet包,右键依赖项,管理NuGet程序包,然后选择浏览,搜索AutoMapper,安装;也可以在vs中使用打开工具-库程序包管理器-程序包管理控制平台,输入“Install-Package AutoMapper”命令
1、第一种情况,两个类字段都一样的情况
(1)首先,建立一个model
public class Student { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } }
(2)建立一个需要映射的类
public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } }
(3)一个简单的映射,由于Student和StudentDTO类字段名称一样,类型相同.需要将Student类的对象映射到StudentDTO类的对象上面。需要对AutoMapper进行如下配置:
//AutoMapper-10.0.0版本 MapperConfiguration config = new MapperConfiguration ( mp => mp.CreateMap<Student, StudentDTO>() // 给config进行配置映射规则 ); var mapConfig = config.CreateMapper(); var studentDtoData = mapConfig.Map<List<StudentDTO>>(new List<Student> { new Student { ID = 1, Name = "hello", Address = "test", Mobile = "110" }}); //映射
(4)完整代码
//AutoMapper-10.0.0版本 using AutoMapper; using System; using System.Collections.Generic; using System.Linq; namespace EFCoreDemo { class Program { static void Main(string[] args) { MapperConfiguration config = new MapperConfiguration ( mp => mp.CreateMap<Student, StudentDTO>() // 给config进行配置映射规则 ); var mapConfig = config.CreateMapper(); var studentDtoData = mapConfig.Map<List<StudentDTO>>(new List<Student> { new Student { ID = 1, Name = "hello", Address = "test", Mobile = "110" }}); //映射 StudentDTO studentDTO = studentDtoData.FirstOrDefault(); Console.WriteLine($"{studentDTO.ID},{studentDTO.Name},{studentDTO.Address},{studentDTO.Mobile}"); Console.ReadKey(); } } public class Student { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } } }
(5)执行结果如下:
2、第二种映射情况:不同字段名称,甚至不同类型的两个类如何映射呢,那就要手动的映射相应字段了。
(1)映射
//AutoMapper-10.0.0版本 MapperConfiguration config = new MapperConfiguration ( mp => mp.CreateMap<Student, StudentDTO>() // 给config进行配置映射规则 .ForMember(stdto => stdto._id, st => st.MapFrom(p => p.ID > 0 ? p.ID : -1)) // 指定映射字段 .ForMember(stdto => stdto._name, st => st.MapFrom(p => p.Name)) .ForMember(stdto => stdto._address, st => st.MapFrom(p => p.Address)) .ForMember(stdto => stdto._mobile, st => st.MapFrom(p => p.Mobile)) ); var mapConfig = config.CreateMapper(); var studentDtoData = mapConfig.Map<StudentDTO>(new Student { ID = 1, Name = "hello", Address = "test", Mobile = "110" });
(2)完整代码如下
using AutoMapper; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; namespace EFCoreDemo { class Program { static void Main(string[] args) { //AutoMapper-10.0.0版本 MapperConfiguration config = new MapperConfiguration ( mp => mp.CreateMap<Student, StudentDTO>() // 给config进行配置映射规则 .ForMember(stdto => stdto._id, st => st.MapFrom(p => p.ID > 0 ? p.ID : -1)) // 指定映射字段 .ForMember(stdto => stdto._name, st => st.MapFrom(p => p.Name)) .ForMember(stdto => stdto._address, st => st.MapFrom(p => p.Address)) .ForMember(stdto => stdto._mobile, st => st.MapFrom(p => p.Mobile)) ); var mapConfig = config.CreateMapper(); var studentDtoData = mapConfig.Map<StudentDTO>(new Student { ID = 1, Name = "hello", Address = "test", Mobile = "110" }); Console.WriteLine($"{studentDtoData._id},{studentDtoData._name},{studentDtoData._address},{studentDtoData._mobile}"); Console.ReadKey(); } } public class Student { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } } public class StudentDTO { public int _id { get; set; } public string _name { get; set; } public string _address { get; set; } public string _mobile { get; set; } } }
(3)执行结果
三、AutoMapper详解
1、简单的例子
public class Student { public int ID { get; set; } public string Name { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } } public void Map() { var config = new MapperConfiguration(cfg => cfg.CreateMap<Student, StudentDTO>()); var mapper = config.CreateMapper(); Student stu = new Student{ ID = 1, Name = "test" }; StudentDTO studto = mapper.Map<StudentDTO>(stu); }
2 、注册
在使用 Map
方法之前,首先要告诉 AutoMapper 什么类可以映射到什么类。
var config = new MapperConfiguration(cfg => cfg.CreateMap<Student, StudentDTO>());
每个 应用程序只能进行一次配置。所以注册的最佳位置是在应用程序启动中,例如 ASP.NET 应用程序的 Global.asax 文件或者.Core中的StartUp.cs
3、Profile
Profile
是组织映射的另一种方式。新建一个类,继承 Profile
,并在构造函数中配置映射。
public class StudentProfile : Profile { public StudentProfile () { CreateMap<Student, StudentDTO>(); } } var config = new MapperConfiguration(cfg => { cfg.AddProfile<StudentProfile >(); });
AutoMapper 也可以在指定的程序集中扫描从 Profile
继承的类,并将其添加到配置中。
var config = new MapperConfiguration(cfg => { // 扫描当前程序集 cfg.AddMaps(System.AppDomain.CurrentDomain.GetAssemblies()); // 也可以传程序集名称(dll 名称) cfg.AddMaps("Test"); });
4、命名约定
默认情况下,AutoMapper 基于相同的字段名映射,并且是 不区分大小写 的。但有时,我们需要处理一些特殊的情况。
-
SourceMemberNamingConvention
表示源类型命名规则DestinationMemberNamingConvention
表示目标类型命名规则
LowerUnderscoreNamingConvention
和 PascalCaseNamingConvention
是 AutoMapper 提供的两个命名规则。前者命名是小写并包含下划线,后者就是帕斯卡命名规则(每个单词的首字母大写)。如果源类型和目标类型分别采用了 下划线命名法 和 驼峰命名法,那么就需要指定命名规则,使其能正确映射。
public class Student { public int Id { get; set; } public string MyName { get; set; } }
public class StudentDTO { public int ID { get; set; } public string My_Name { get; set; } } public void Map() { var config = new MapperConfiguration(cfg => { cfg.CreateMap<Student, StudentDTO>(); cfg.SourceMemberNamingConvention = new PascalCaseNamingConvention(); cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention(); }); var mapper = config.CreateMapper(); Student stu = new Student { Id = 2, MyName = "test" }; StudentDTO dto = mapper.Map<StudentDTO >(stu); }
5、配置可见性
默认情况下,AutoMapper 仅映射 public
成员,但其实它是可以映射到 private
属性的。
var config = new MapperConfiguration(cfg => { cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.SetMethod.IsPrivate; cfg.CreateMap<Source, Destination>(); });
6、 全局属性/字段过滤
默认情况下,AutoMapper 尝试映射每个公共属性/字段。以下配置将忽略字段映射。
var config = new MapperConfiguration(cfg => { cfg.ShouldMapField = fi => false; });
7、 识别前缀和后缀
var config = new MapperConfiguration(cfg => { cfg.RecognizePrefixes("My"); cfg.RecognizePostfixes("My"); }
8、 替换字符
var config = new MapperConfiguration(cfg => { cfg.ReplaceMemberName("Ä", "A"); });
9、 调用构造函数
public class Student { public string Name { get; set; } public int Price { get; set; } } public class StudentDTO { public string Name { get; } public int Price { get; } public StudentDTO(string name, int price) { Name = name; Price = price * 2; } }
AutoMapper 会自动找到相应的构造函数调用。如果在构造函数中对参数做一些改变的话,其改变会反应在映射结果中。如上例,映射后 Price
会乘 2。
禁用构造函数映射:禁用构造函数映射的话,目标类要有一个无参构造函数。
var config = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
10、 数组和列表映射
数组和列表的映射比较简单,仅需配置元素类型,定义简单类型如下:
public class Source { public int Value { get; set; } } public class Destination { public int Value { get; set; } } 映射: var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>(); }); IMapper mapper = config.CreateMapper(); var sources = new[] { new Source { Value = 5 }, new Source { Value = 6 }, new Source { Value = 7 } }; 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);
具体来说,支持的源集合类型包括:
-
- IEnumerable
- IEnumerable
- ICollection
- ICollection
- IList
- IList
- List
- Arrays
11、处理空集合
映射集合属性时,如果源值为 null
,则 AutoMapper 会将目标字段映射为空集合,而不是 null
。这与 Entity Framework 和 Framework Design Guidelines 的行为一致,认为 C# 引用,数组,List,Collection,Dictionary 和 IEnumerables 永远不应该为 null
。
12、 集合中的多态
public class Student { public int ID { get; set; } public string Name { get; set; } } public class Student2: Student { public string DeptName { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } } public class StudentDTO2 :StudentDTO { public string DeptName { get; set; } } 数组映射代码如下: var config = new MapperConfiguration(cfg => { cfg.CreateMap<Student, StudentDTO>().Include<Student2, StudentDTO2>(); cfg.CreateMap<Student2, StudentDTO2>(); }); IMapper mapper = config.CreateMapper(); var stu= new[] { new Student{ ID = 1, Name = "小名" }, new Student2{ ID = 2, Name = "晓红", DeptName = "是是" } }; var dto = mapper.Map<Student[], StudentDTO[]>(stu);
可以看到,映射后,DTO中两个元素的类型,一个是 StudentDTO,一个是 StudentDTO2
,即实现了父类映射到父类,子类映射到子类。如果去掉Include
方法,则映射后 DTO中两个元素的类型均为 StudentDTO。
13、 方法到属性映射
AutoMapper 不仅能实现属性到属性映射,还可以实现方法到属性的映射,并且不需要任何配置,方法名可以和属性名一致,也可以带有 Get
前缀。下例的 Student.GetFullName()
方法,可以映射到 StudentDTO.FullName
属性。
public class Student { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string GetFullName() { return $"{FirstName} {LastName}"; } } public class StudentDTO { public int ID { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get; set; } }
14、 自定义映射
当源类型与目标类型名称不一致时,或者需要对源数据做一些转换时,可以用自定义映射。
public class Student { public int ID { get; set; } public string Name { get; set; } public DateTime JoinTime { get; set; } } public class StudentDTO { public int stuID { get; set; } public string stuName { get; set; } public int JoinYear { get; set; } } 如上例,ID 和 stuID 属性名不同,JoinTime 和 JoinYear 不仅属性名不同,属性类型也不同。 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Student, StudentDTO>() .ForMember("stuID", opt => opt.MapFrom(src => src.ID)) .ForMember(dest => dest.stuName, opt => opt.MapFrom(src => src.Name)) .ForMember(dest => dest.JoinYear, opt => opt.MapFrom(src => src.JoinTime.Year)); });
15、扁平化映射
对象-对象映射的常见用法之一是将复杂的对象模型并将其展平为更简单的模型。
public class Student { public int ID { get; set; } public string Name { get; set; } public Department Department { get; set; } } public class Department { public int ID { get; set; } public string Name { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public int DepartmentID { get; set; } public string DepartmentName { get; set; } }
如果目标类型上的属性,与源类型的属性、方法都对应不上,则 AutoMapper 会将目标成员名按驼峰法拆解成单个单词,再进行匹配。例如上例中,StudentDTO.DepartmentID
就对应到了 Student.Department.ID
。
16、IncludeMembers
如果属性命名不符合上述的规则,而是像下面这样:
public class Student { public int ID { get; set; } public string Name { get; set; } public Department Department { get; set; } } public class Department { public int DepartmentID { get; set; } public string DepartmentName { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public int DepartmentID { get; set; } public string DepartmentName { get; set; } } Department 类中的属性名,直接跟 StudentDTO类中的属性名一致,则可以使用 IncludeMembers 方法指定。 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Student, StudentDTO>().IncludeMembers(e => e.Department); cfg.CreateMap<Department, StudentDTO>(); });
17、嵌套映射
有时,我们可能不需要展平。看如下例子:
public class Student { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } public Department Department { get; set; } } public class Department { public int ID { get; set; } public string Name { get; set; } public string Heads { get; set; } } public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public DepartmentDTO Department { get; set; } } public class DepartmentDTO { public int ID { get; set; } public string Name { get; set; } } 我们要将 Student映射到 StudentDTO,并且将 Department 映射到 DepartmentDTO。 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Student, StudentDTO>(); cfg.CreateMap<Department, DepartmentDTO>(); });
官方文档:https://docs.automapper.org/en/latest/