create database MvcUnit4; go use MvcUnit4; go create table Product ( Id bigint primary key, ProductName varchar(30), CategoryName varchar(30), Price d ...
create database MvcUnit4; go use MvcUnit4; go create table Product ( Id bigint primary key, ProductName varchar(30), CategoryName varchar(30), Price decimal(10,2), Remark varchar(200), CreatedUserId bigint, UpdatedUserId bigint, CreatedTime datetime, UpdatedTime datetime, Deleted bit ); insert into Product values(1,'C# 入門','.Net 開發',25,'',1,1,getdate(),getdate(),0); insert into Product values(2,'Sql基礎','資料庫',25,'',1,1,getdate(),getdate(),0); insert into Product values(3,'SQL高級','資料庫',120,'',1,1,getdate(),getdate(),0); insert into Product values(4,'Ado.Net','資料庫訪問技術',25,'',1,1,getdate(),getdate(),0); insert into Product values(5,'EntityFramework','資料庫訪問技術',120,'',1,1,getdate(),getdate(),0); insert into Product values(6,'C#高級','.Net 開發',140,'',1,1,getdate(),getdate(),0); insert into Product values(7,'Asp.net Mvc Core','.Net 開發',25,'',2,2,getdate(),getdate(),0); insert into Product values(8,'MySql基礎','資料庫',25,'',2,2,getdate(),getdate(),0); go create table UserInfo ( Id bigint primary key, UserName varchar(30), NickName varchar(30), Pwd varchar(50), CreatedUserId bigint, UpdatedUserId bigint, CreatedTime datetime, UpdatedTime datetime, Deleted bit ); insert into UserInfo values(1,'renwoxing','任我行','123456',0,0,getdate(),getdate(),0); insert into UserInfo values(2,'xiaoming','小明','123456',0,0,getdate(),getdate(),0);
1. 視圖模型-ViewModel
1. 為什麼要建立視圖模型
不同的業務場景,我們最好是建立不同的ViewModel來與與之對應。我們弄個實際開發的例子來進行說明。
// 父類數據實體 public class BaseEntity { public long Id{ get;set;} public long CreatedUserId { get; set;} public long UpdatedUserId { get; set;} public DateTime CreatedTime { get; set; }= DateTime.Now; public DateTime UpdatedTime { get; set; }= DateTime.Now; public bool Deleted { get; set; } = false; } // 商品數據實體 class Product : BaseEntity { public String ProductName {get; set;} public decimal Price { get; set; } public String CategoryName { get; set; } public String Remark { get; set; } } // 用戶數據實體 class UserInfo : BaseEntity{ public String UserName { get;set;} public String NickName { get;set;} public String Pwd { get;set;} }
此時前端需要展示的數據項如下:
商品名,價格,分類,添加人,維護人,最後維護時間
此時你的商品視圖模型應該是:
class ProductViewModel{ public long Id; public String ProductName {get; set;} public decimal Price { get; set; } public String CategoryName { get; set; } public String CreatedNickName { get; set; } public String UpdatedNickName { get; set; } public DateTime UpdatedTime { get; set; } }
使用視圖模型的好處:
面對那些業務場景不需要的欄位我們不應該返回給前端,
-
方便此業務場景的維護,就算將來當前業務場景發生變化,也不至於影響到其他同學的調用。
-
欄位太多,對於其他調用者來說不太友好,別人不知道這個欄位是幹嘛用的,特別是現在流行微服務開發,當我們給別人提供介面時,切記要記得“按需所給” ,否則別人可能會為了你這些“沒用的欄位” 而 去大費周章的去東挪西湊。
-
更符合當前的DDD開發模式。
添加或者修改的視圖:
// 父類輸入數據 class BaseInput{ public long Id{ get;set;} public long CreatedUserId { get; set;} public long UpdatedUserId { get; set;} public DateTime CreatedTime => DateTime.Now; public DateTime UpdatedTime => DateTime.Now; public bool Deleted => false; } class ProductInput : BaseInput{ public String ProductName {get; set;} public decimal Price { get; set; } public String CategoryName { get; set; } public String Remark {get;set;} }
BaseInput 與 BaseEntity 雖然 欄位一樣,但是所處的業務領域不一樣。比如我們可能會在BaseEntity 類中對欄位Id 標識 [Key] 特性來說明它是主鍵欄位,而BaseInput 類中的Id 則只是作為一個輸入參數而已,換句話說,此Id而非彼Id。
ProductInput 類 雖然 與 Product 類 欄位一樣,但這隻是個例,事實在微服務開發過程中,很多時候方法的入參欄位列表與數據實體的欄位列表是不一樣的。目前大家只需要掌握這種思想即可,隨著大家的開發經驗的積累,會慢慢的體會到這種模式的好處。
2. 屬性映射
雖然建立視圖模型給我們帶來了業務領域驅動的好處,但是同樣也給我們帶來了一些編碼上的麻煩,代碼如下:
public class ProductController:Controller { private readonly IdWorker _idWorker; private readonly IProductService _productService; public ProductController(IProductService productService) { _productService = productService; _idWorker = SnowflakeUtil.CreateIdWorker(); } public IActionResult Create(ProductInput input) { // 將ProductInput 轉換為 Product 數據實體對象 // (實際開發場景下,Input實體轉換為數據實體過程應放至業務邏輯層) Product entity = new() { Id = _idWorker.NextId(), // 雪花Id CategoryName = input.CategoryName, Price = input.Price, ProductName = input.ProductName }; _productService.Save(entity); return Ok("添加成功"); } }
如果我們每一個業務對象都需要這樣手動的為每個屬性賦值的話,那對於我們程式員來說簡直就是把技術活變成了體力活了。接下來我們需要介紹另外一款組件:AutoMap自動映射
2. AutoMap 組件-自動映射
需要安裝的包:
-
AutoMapper.Extensions.Microsoft.DependencyInjection
快速開始
首先,配置映射關係
public class OrganizationProfile : Profile { public OrganizationProfile() { CreateMap<Foo, FooDto>(); } }
如果是控制台應用,則:
var configuration = new MapperConfiguration(cfg => { //cfg.CreateMap<Foo, Bar>(); cfg.AddProfile<OrganizationProfile>();//或者cfg.AddProfile(new OrganizationProfile()); }); var mapper=configuration.CreateMapper();//或者var mapper=new Mapper(configuration); var dest=mapper.Map<OrderDto>(order);
如果ASP.NET Core應用,則(需安裝AutoMapper.Extensions.Microsoft.DependencyInjection):
//1. 配置服務里Add,入參類型為params build.Services.AddAutoMapper(typeof(OrganizationProfile)); //2. 然後在Controller里使用即可: public XXXController(IMapper mapper) { _mapper = mapper; var dest=mapper.Map<OrderDto>(order); }
1.常見配置
1.1 Profile的配置
除了上述的手動添加profile,還可以自動掃描profile並添加:
//方法1 var configuration = new MapperConfiguration(cfg => cfg.AddMaps(myAssembly)); //方法2:通過程式集名 var configuration = new MapperConfiguration(cfg => cfg.AddMaps(new [] { "Foo.UI", // 程式集名稱 "Foo.Core" }); );
1.2 命名方式 camelCase/PascalCase
作用:駝峰命名與Pascal命名的相容。
以下全局配置會映射property_name到PropertyName
builder.Services.AddAutoMapper(cfg => { cfg.AddProfile<CustomerProfile>(); // propertyName ------> PropertyName 預設是支持的 cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention(); cfg.DestinationMemberNamingConvention = new PascalCaseNamingConvention(); });
或者針對某個profile進行配置(這種方式全局通用):
public class OrganizationProfile : Profile { public OrganizationProfile() { SourceMemberNamingConvention = new LowerUnderscoreNamingConvention(); DestinationMemberNamingConvention = new PascalCaseNamingConvention(); //Put your CreateMap... Etc.. here } }
1.3 映射時針對某些字元進行替換
var configuration = new MapperConfiguration(c => { c.ReplaceMemberName("Ä", "A"); c.ReplaceMemberName("í", "i"); c.ReplaceMemberName("Airlina", "Airline"); });
進行以上配置之後,會自動將Äbc映射到Abc上,將íng映射到ing上,將AirlinaMark映射到AirlineMark上。
1.4 映射時匹配首碼或尾碼
var configuration = new MapperConfiguration(cfg => { cfg.RecognizePrefixes("frm"); //cfg.RecongizePostfixes("尾碼"); cfg.CreateMap<Source, Dest>(); });
這樣frmValue就可以map到Value上。
Automapper預設匹配了Get首碼,如果不需要可以清除:
cfg.ClearPrefixes();//清除所有首碼
★1.5控制哪些屬性和欄位能夠被映射
使用ShouldMapField和ShouldMapProperty
cfg.ShouldMapField = fi => false; cfg.ShouldMapProperty = pi =>pi.GetMethod != null && (pi.GetMethod.IsPublic || pi.GetMethod.IsPrivate);
預設所有public的field和property都會被map,也會map private 的setter,但是不會map整個property都是internal/private的屬性。
1.6 提前編譯
預設是調用的時候才編譯映射,但是可以要求AutoMapper提前編譯,但是可能會花費點時間:
var configuration = new MapperConfiguration(cfg => {}); configuration.CompileMappings();
2. Projection(映射)
AutoMapper只會映射扁平的類,嵌套的類,繼承的類,需要進行手動配置。成員名稱不一致時,也要手動配置映射。
AutoMapper預設會自動映射以下類型,並且映射時會先清空dest對應成員的數據:
-
IEnumerable
-
IEnumerable<T>
-
ICollection
-
ICollection<T>
-
IList
-
IList<T>
-
List<T>
-
Arrays
這幾個集合之間可以相互映射,如:
mapper.Map<Source[], IEnumerable<Destination>>(sources);
★2.1 手動控制某些成員的映射
ForMember+MapFrom
// Configure AutoMapper var configuration = new MapperConfiguration(cfg => cfg.CreateMap<CalendarEvent, CalendarEventForm>() .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date)) .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour)) .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)) );
將src的Date.Date映射到dest的EventDate成員上
★2.2 嵌套(Nested)類和繼承類映射
某些成員可能是一個類,那麼這個類也要配置映射。同理一個類的父類也要配置映射。
var config = new MapperConfiguration(cfg => { cfg.CreateMap<OuterSource, OuterDest>(); cfg.CreateMap<InnerSource, InnerDest>(); });
★2.3 映射繼承與多態 Include/IncludeBase
假如ChildSource繼承ParentSource,ChildDestination繼承ParentDestination。並且有這麼一個業務,ParentSource src=new ChildSource()需要把src轉為ParentDestination。
直接轉的話肯定會有member丟失,所以要進行如下配置:
var configuration = new MapperConfiguration(c=> { c.CreateMap<ParentSource, ParentDestination>() .Include<ChildSource, ChildDestination>(); c.CreateMap<ChildSource, ChildDestination>(); });
或者也可以這麼寫:
CreateMap<ParentSource,ParentDestination>(); CreateMap<ChildSource,ChildDestination>() .IncludeBase<ParentSource,ParentDestination>();
如果有幾十個類都繼承了ParentSource和ParentDestination,那麼上述兩種方法就太啰嗦了,可以這麼寫:
CreateMap<ParentSource,ParentDestination>().IncludeAllDerived();
CreaetMap<ChildSource,ChildDestination>();
更複雜的用法參考:Mapping-inheritance
2.4 構造函數映射
如果dest構造函數的入參名和src的某個member一致,則不用手動配置,automapper會自動支持:
public class Source{ public Source(int value) { this.Value = value; } public int Value { get; set; } } public class SourceDto { public SourceDto(int value) { _value = value; } private int _value; public int Value { get { return _value; } } }
如果這裡構造的入參不叫value而叫value2,則要進行如下配置:
var configuration = new MapperConfiguration(cfg => cfg.CreateMap<Source, SourceDto>() .ForCtorParam("value2", opt => opt.MapFrom(src => src.Value)) );
也可以禁用構造函數映射:
var configuration = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
也可以配置什麼情況下不用構造函數映射:
var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = ci => !ci.IsPrivate);//不匹配私有構造函數
2.5 複雜類映射成扁平類
public class Src { public Customer Customer {get;set;} public int GetTotal() { return 0; } } public class Customer { public string Name {get;set;} } public class Dest { public string CustomerName {get;set;} public int Total {get;set;} }
則src可以自動映射成dest,包括CustomerName和Total欄位。這種與手動配置
cfg.CreateMap<Src,Dest>().ForMember(d=>d.CustomerName,opt=>opt.MapFrom(src=>src.Customer.Name))
然後進行映射的方式類似。
映射時AutoMapper發現,src里沒有CustomerName這個成員,則會將dest的CustomerName按照PascalCase的命名方式拆分為獨立的單詞。所以CustomerName會映射到src的Customer.Name。如果想禁用這種自動映射,則調用cfg.DestinationMemberNamingConvention = new ExactMatchNamingConvention();
使用精確映射。
如果感覺AutoMapper的這種基於PascalCase命名拆分的自動映射沒法滿足你的需要,則還可以手動指定某些成員的映射:
class Source { public string Name { get; set; } public InnerSource InnerSource { get; set; } public OtherInnerSource OtherInnerSource { get; set; } } class InnerSource { public string Name { get; set; } public string Description { get; set; } } class OtherInnerSource { public string Name { get; set; } public string Description { get; set; } public string Title { get; set; } } class Destination { public string Name { get; set; } public string Description { get; set; } public string Title { get; set; } } cfg.CreateMap<Source, Destination>().IncludeMembers(s=>s.InnerSource, s=>s.OtherInnerSource); cfg.CreateMap<InnerSource, Destination>(MemberList.None); cfg.CreateMap<OtherInnerSource, Destination>(); var source = new Source { Name = "name", InnerSource = new InnerSource{ Description = "description" }, OtherInnerSource = new OtherInnerSource{ Title = "title",Description="descpripiton2" } }; var destination = mapper.Map<Destination>(source); destination.Name.ShouldBe("name"); destination.Description.ShouldBe("description"); destination.Title.ShouldBe("title");
IncludeMembers參數的順序很重要,這也就是dest的Description為“description”而不是“description2”的原因,因為InnerSource的Description屬性最先匹配到了Destination的Description屬性。
IncludeMembers相當於把子類打平添加到了src里,併進行映射。
★2.6 映射反轉(Reverse Mapping)
reverse mapping一般在CreateMap方法或者ForMember等方法之後,相當於src和dest根據你自己的配置可以相互映射,少寫一行代碼:
cfg.CreateMap<Order, OrderDto>().ReverseMap(); //等同於以下兩句 cfg.CreateMap<Order,OrderDto>(); cfg.CreateMap<OrderDto,Order>();
如果還想對reverse map進行自定義(大多數情況下都不需要),則可以使用ForPath:
cfg.CreateMap<Order, OrderDto>() .ForMember(d => d.CustomerName, opt => opt.MapFrom(src => src.Customer.Name)) .ReverseMap() .ForPath(s => s.Customer.Name, opt => opt.MapFrom(src => src.CustomerName));
註意:
如果reverse之前定義了一些諸如ForMember之類的約束,這些約束是不會自動reverse的,需要手動配置。以下代碼配置了不管從Order映射到OrderDto還是從OrderDto映射到Order,都忽略CustomerName屬性。
cfg.CreateMap<Order, OrderDto>() .ForMember(d => d.CustomerName, opt => opt.Ignore()) .ReverseMap() .ForMember(d => d.CustomerName, opt => opt.Ignore())
2.7 使用特性映射
(C#稱作特性,Java叫註解)
[AutoMap(typeof(Order))] public class OrderDto {}
等同於CreateMap<Order,OrderDto>()
。然後配置的時候用AddMaps方法:
var configuration = new MapperConfiguration(cfg => cfg.AddMaps("MyAssembly")); var mapper = new Mapper(configuration);
特性里還有如下參數供設置:
ReverseMap (bool) ConstructUsingServiceLocator (bool) MaxDepth (int) PreserveReferences (bool) DisableCtorValidation (bool) IncludeAllDerived (bool) TypeConverter (Type)
TypeConverter (Type)映射註解的更多信息參考:Attribute-mapping
2.8 動態類型Dynamic到普通類型的映射
預設就支持,不用手動CreateMap
★2.9 泛型映射
public class Source<T> {} public class Destination<T> {} var configuration = new MapperConfiguration(cfg => cfg.CreateMap(typeof(Source<>), typeof(Destination<>)));
註意:CreateMap不需要傳具體的T
★3. 擴展IQueryable(與EF等ORM配合使用)
需要安裝nuget包:AutoMapper.EF6
這個功能存在的意義是為瞭解決一些orm框架返回的是IQueryable類型,使用一般的mapper.Map做轉換時,會查詢出來整行數據,然後再挑選出來某些欄位做映射,會降低性能的問題。解決方法是使用ProjectTo:
public class OrderLine { public int Id { get; set; } public int OrderId { get; set; } public Item Item { get; set; } public decimal Quantity { get; set; } } public class Item { public int Id { get; set; } public string Name { get; set; } } public class OrderLineDTO { public int Id { get; set; } public int OrderId { get; set; } public string Item { get; set; } public decimal Quantity { get; set; } } var configuration = new MapperConfiguration(cfg => cfg.CreateMap<OrderLine, OrderLineDTO>() .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); public List<OrderLineDTO> GetLinesForOrder(int orderId) { using (var context = new orderEntities()) { return context.OrderLines.Where(ol => ol.OrderId == orderId) .ProjectTo<OrderLineDTO>(configuration).ToList(); } } //這樣查Item表的時候,只會select name欄位。
3.1 枚舉映射
大多數情況下都不需要進行此項的配置。考慮以下兩個枚舉:
public enum Source { Default = 0, First = 1, Second = 2 } public enum Destination { Default = 0, Second = 2 }
如果想把Source.First映射到Destination.Default上,則需要安裝AutoMapper.Extensions.EnumMapping
nuget包,然後進行如下配置:
internal class YourProfile : Profile { public YourProfile() { CreateMap<Source, Destination>() .ConvertUsingEnumMapping(opt => opt // optional: .MapByValue() or MapByName(), without configuration MapByValue is used .MapValue(Source.First, Destination.Default) ) .ReverseMap(); } }
預設情況下AutoMapper.Extensions.EnumMapping 會將源枚舉里所有的數據根據枚舉值或枚舉名稱映射到目標枚舉上。如果找不到且啟用了EnumMappingValidation,則會拋出異常。
4.進階
★4.1 類型轉換(可針對所有的maps生效)
當從可空類型與不可空類型相互轉換時,當string與int等轉換時,就需要自定義類型轉換。一般有以下三種方法:
void ConvertUsing(Func<TSource, TDestination> mappingFunction); void ConvertUsing(ITypeConverter<TSource, TDestination> converter);