一、概要
1、DTO
DTO(Data Transfer Object)就是數據傳輸對象,說白了就是一個對象,只不過里邊全是數據而已。
2、為什么要用DTO?
- DTO更注重數據,對領域對象進行合理封裝,從而不會將領域對象的行為過分暴露給表現層
- DTO是面向UI的需求而設計的,而領域模型是面向業務而設計的。因此DTO更適合於和表現層的交互,通過DTO我們實現了表現層與領域Model之間的解耦,因此改動領域Model不會影響UI層
- DTO說白了就是數據而已,不包含任何的業務邏輯,屬於瘦身型的對象,使用時可以根據不同的UI需求進行靈活的運用
現在我們既然知道了使用DTO的好處,那么我們肯定也想馬上使用它,但是這里會牽扯一個問題:怎樣實現DTO和領域Model之間的轉換?有兩個思路,我們要么自己寫轉換代碼,要么使用工具。不過就應用而言,我還是覺得用工具比較簡單快捷,那就使用工具吧。其實這樣的轉換工具很多,不過我還是決定使用AutoMapper,因為它足夠輕量級,而且也非常流行,國外的大牛們都使用它。使用AutoMapper可以很方便的實現DTO和領域Model之間的轉換,它是一個強大的Object-Object Mapping工具。
3、AutoMapper是什么?
AutoMapper是一個對象-對象映射器。對象-對象映射通過將一種類型的輸入對象轉換為另一種類型的輸出對象來工作。使AutoMapper變得有趣的是,它提供了一些有趣的約定,以免去搞清楚如何將類型A映射為類型B。只要類型B遵循AutoMapper既定的約定,就需要幾乎零配置來映射兩個類型。
4、為什么使用AutoMapper?
“為什么使用對象-對象映射?” 映射可以在應用程序中的許多地方發生,但主要發生在層之間的邊界中,例如,UI /域層之間或服務/域層之間。一層的關注點通常與另一層的關注點沖突,因此對象-對象映射導致分離的模型,其中每一層的關注點僅會影響該層中的類型。
二、AutoMapper簡單實例
首先,在項目中引用NuGet包,右鍵依賴項,管理NuGet程序包,然后選擇瀏覽,搜索AutoMapper,安裝;也可以在vs中使用打開工具-庫程序包管理器-程序包管理控制平台,輸入“Install-Package AutoMapper”命令
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; } }
建立一個需要映射的類
public class StudentDTO { public int ID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Mobile { get; set; } }
一個簡單的映射,由於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" }}); //映射
完整代碼
//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; } } }
執行結果如下:
2、第二種映射情況:不同字段名稱
不同字段名稱,甚至不同類型的兩個類如何映射呢,那就要手動的映射相應字段了。
映射:
//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" });
完整代碼如下:
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; } } }
執行結果
三、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/