# 如何在 .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`