# 如何在 .NET 使用 AutoMapper [![hackmd-github-sync-badge](https://hackmd.io/DbtaXmo7S-GC3pHbF3wCXg/badge)](https://hackmd.io/DbtaXmo7S-GC3pHbF3wCXg) ## 前言 一個應用程式在設計時通常會進行多層次分層,秉持著關注點分離的原則,一般不會將同一個DTO跨越多個分層進行傳輸,而是在每層間將傳入 DTO 轉換成另一個 DTO 後,傳入下個分層。 舉例來可能會進行這樣的拆分,Entity←(Domain Layer)→Service DTO←(Application Layer)→ViewModel,當要進行資料更新時,Application Layer 將 ViewModel 轉換為 Service DTO 傳入至 Domain Layer 的 Service 當參數,Service 再將 Service DTO 轉換成 Entity 透過 Repository 寫入資料(Entity Framework 本身就是實作 Repository Pattern 的架構),在處理這些 DTO 轉換的屬性/欄位設值是非常麻煩的,所以都會寫映射(Reflaction) API 來簡化操作,而 AutoMapper 是比較常用的一個映射套件,本身[文件](https://docs.automapper.org/en/latest/index.htm)撰寫非常詳細,相對好上手。 ## AutoMapper 的使用方式 AutoMapper的使用方式如下: ```csharp // 建立Configuration設定class之間的映射關係 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Order, OrderDto>(); }); // 驗證Configuration的設置,Destination有的Member,Source一定要有,或是有特別處理,否則會throw AutoMapperConfigurationException config.AssertConfigurationIsValid(); // 建立Mapper var mapper = config.CreateMapper(); // 建立Destination dest,並將source的值映射至dest Destination dest mapper.Map<Destination>(source); // 將source的值值映射至已存在dest mapper.Map(source, dest); ``` ## 設置 正常來說不會有動態變動class之間的映射關係,所以MapperConfiguration只需要建置一份就好,一般會寫在Startup.cs裡,.NET 6則會新C#語法關係,預設沒有Startup.cs,所以就寫在Program.cs裡 ### class 間常用的映射設置方式 ```csharp var config = new MapperConfiguration(cfg => { // 單一型別轉換設置,設定Source可轉換成Destination cfg.CreateMap<Source, Destination>(); // 使用ConvertUsing,用Expression<Func<Source, Destination>>直接定義兩個型別的轉換關係 // 這邊用來將string的值去空白 cfg.CreateMap<string?, string?>() .ConvertUsing(x => x == null ? x : x.Trim()); // 把設置寫在Profile的引用方式 cfg.AddProfile(OtherProfile); }); //... // 把設置單獨寫在class提供別人做引用 public class OtherProfile : Profile { public OtherProfile() { cfg.CreateMap<Source1, Destination1>(); } } ``` ### Property 間常用的映射設置方式 #### 前/後綴詞 ```csharp var config = new MapperConfiguration(cfg => { // 單一class設置,設定Source可轉換成Destination cfg.CreateMap<Source, Destination>(); // 設定來源前綴詞 // 例如:Source.Name->Destination.Name和Source.PrefixName->Destination.Name都支援 // 如果Source同時有Name和PrefixName,則以Source裡先定義的Member優先 cfg.RecognizePrefixes("Prefix"); // 設定來源後綴詞 cfg.RecognizePostfixes("Postfix"); // 設定目標前綴詞 // 例如:Source.Name->Destination.Name和Source.Name->Destination.PrefixName都支援 // 如鬼Destination同時有Name和PrefixName,兩個Member得值都會是Source.Name的值 cfg.RecognizeDestinationPrefixes("Prefix"); // 設定目標後綴詞 cfg.RecognizeDestinationPostfixes("Postfix"); // 預設有加入Get為前綴詞,不想要這個前綴詞則呼叫此API cfg.ClearPrefixes(); }); //...... // RecognizePrefixes引用原則 class Destination { public string Name { get; set; } } // 如果Source.Name定義在前面 class Source { public string Name { get; set; } // Destination.Name為Source.Name的值 public string PrefixName { get; set; } } // 如果Source.PrefixName定義在前面 class Source { public string PrefixName { get; set; } // Destination.Name為Source.PrefixName的 public string Name { get; set; }值 } ```` #### 針對單一 Member 設定 ```csharp var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>() // 不映設值給Destination.Prop1 // 通常是在Destination有此Member,Source沒有此Member時使用 .ForMember(desc => desc.Prop1, opt => opt.Ignore()) // Source和Destination名稱不一樣,設定對應關係 .ForMember(dest => dest.DestProp2, opt => opt.MapFrom(src => src.SourceProp2)) // 映射物件時,不從Source給值,而是額外設值 .ForMember(desc => desc.DateProp3, opt => opt.MapFrom(src => DateTime.Now)) // 只有在Source.IntProp4大於等於0才會映設到Destination.IntProp4 .ForMember(dest => dest.IntProp4, opt => opt.Condition(src => (src.IntProp4 >= 0))) // Source.Prop5為Null時,將Destination.Prop5設值為"Other Value",反之從值為Source.Prop5 .ForMember(dest => dest.Prop5, opt => opt.NullSubstitute("Other Value"))); // 如果desc.CreatedTime不為default,則將desc.ModifiedTime設值為現在時間 // 如果desc.CreatedTime為default,則將desc.CreatedTime設值為現在時間 // 主要用於新增與修改寫入不同欄位時使用,作為判斷是否有值的CreatedTime需放置在最後映射 .ForMember(desc => desc.ModifiedTime, opt => { opt.PreCondition((src, desc, context) => desc.CreatedTime != default); opt.MapFrom(src => DateTime.Now); }) .ForMember(desc => desc.CreatedTime, opt => { opt.PreCondition((src, desc, context) => desc.CreatedTime == default); opt.MapFrom(src => DateTime.Now); }); }); class Source { public string? SourceProp2 { get; set; } public int IntProp4 { get; set; } public string? Prop5 { get; set; } } public class Destination { public string? Prop1 { get; set; } public string? Prop2 { get; set; } public DateTime DateProp3 { get; set; } public int IntProp4 { get; set; } public string? Prop5 { get; set; } public DateTime CreatedTime { get; set; } public DateTime? ModifiedTime { get; set; } } ```` #### 反向映射 如果希望Source和Destination兩種型別可以互轉,可以用以下兩種寫法 ```csharp // 分別定義Source和Destination相互間的轉換關係 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>(); cfg.CreateMap<Destination, Source>(); }); //... // 使用反向映射 var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>() .ReverseMap(); }); ``` 使用反向映射有兩個注意的地方,在使用需要評估一下 1. 如果複雜的轉換,需要設置ForPath來定義反向轉換關係。 2. AssertConfigurationIsValid()在反向映射不起作用。 ```csharp var config = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>() .ForMember(dest => dest.Prop2, opt => opt.MapFrom(src => src.Prop1)) .ForMember(dest => dest.Prop5, opt => opt.MapFrom(src => src.Prop3 + "," + src.Prop4)) .ReverseMap() .ForPath(s => s.Prop3, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[0])) .ForPath(s => s.Prop4, opt => opt.MapFrom(src => src.Prop5.Split(new char[] { ',' })[1])); }); var source = new Destination { Prop2 = "123", Prop5 = "111,222" }; Source dest = mapper.Map<Source>(source); // dest.Prop1 = "123" 此對應關係較為單純,所以不用設定ForPath也可以反向轉換 // dest.Prop3 = "111" 如果沒設定ForPath會為null // dest.Prop4 = "222" 如果沒設定ForPath會為null public class Source { public string? Prop1 { get; set; } public string? Prop3 { get; set; } public string? Prop4 { get; set; } } public class Destination { public string? Prop2 { get; set; } public string? Prop5 { get; set; } } ``` ## Dependency Injection 需額外安裝NuGet套件[AutoMapper.Extensions.Microsoft.DependencyInjection](https://www.nuget.org/packages/AutoMapper.Extensions.Microsoft.DependencyInjection/)。 .NET Core 3.x Startup.cs ```csharp public void ConfigureServices(IServiceCollection services) { // 使用Assembly註冊 services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/); // 使用型別註冊該型別所屬Assembly services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/); } ``` .NET 6 Program.cs(預設不使用Startup時的寫法) ```csharp // 使用Assembly註冊 builder.Services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/); // 使用型別註冊該型別所屬Assembly builder.Services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/); ``` AutoMapper 注入至Service或Controller ```csharp public class EmployeesController { private readonly IMapper mapper; public EmployeesController(IMapper mapper) => this.mapper = mapper; } ``` ###### tags: `.NET` `AutoMapper`