PocoEmit遙(yao)遙(yao)領(ling)先于(yu)AutoMapper之打通充血模型的任督二脈
一、充血模型和失血模型
1. 充血模型的優勢
- 充血模型更加OOP
- 充血模型代碼可讀性更好
1.1 充血模型偽代碼
var messageDto = controller.ReadDto();
var message = messageDto.ToEntity();
message.Save();
1.2 失血模型偽代碼
var messageDto = controller.ReadDto();
var message = messageDto.ToEntity();
var messageService = controller.GetMessageService();
messageService.Save(message);
2. 充血模型愛你不容易
- 大部分程序員都知道充血模型好,想實現卻很難
- 大部分業務邏輯都需要依賴外部服務
- 充血模型需要用到外部服務,又不想依賴外部服務的具體實現
- 很容易想到使用IOC的依賴注入來解決
- 我們給DTO注入服務還行,因為IOC參與了controller過程
- 當DTO發生轉化時,新增的服務IOC還是有點力不從心
- 沒法引用外部服務的充血模型氣血不通,業務表達能力大大下降
- 這也是大部分人棄用充血模型的主要原因,不好用還不如不用
- 這個任督二脈PocoEmit可以幫你打通
二、首先來個Case演示一下
- Dto轉化為實體
- 但是實體有更多邏輯依賴外部服務,這些外部服務Dto不見得提供的了
- 這就需要注入
- PocoEmit支持構造函數參數注入和屬性注入
- IMapper對象是默認支持注入的服務
1. Entity比Dto多出來的Mapper可以注入
class MessageDto
{
public string Message { get; set; }
}
class MessageEntity
{
public IMapper Mapper { get; set; }
public string Message { get; set; }
}
2. 轉化并注入的代碼
var mapper = Mapper.Create();
var dto = new MessageDto { Message = "Hello UseMapper" };
MessageEntity message = mapper.Convert<MessageDto, MessageEntity>(dto);
Assert.NotNull(message.Mapper);
三、再演示注入自定義的服務
1. UserDomain比Dto多出來的Repository可以注入
class UserDTO
{
public int Id { get; set; }
public string Name { get; set; }
}
class UserDomain(UserRepository repository, int id, string name)
{
private readonly UserRepository _repository = repository;
public UserRepository Repository
=> _repository;
public int Id { get; } = id;
public string Name { get; } = name;
// ...
}
class UserRepository
{
void Add(UserDomain user) { }
void Update(UserDomain entity) { }
void Remove(UserDomain entity) { }
public static readonly UserRepository Instance = new();
}
2. 注冊、轉化并注入的代碼
- 通過UseDefault可以注入服務
IMapper mapper = Mapper.Create()
.UseDefault(UserRepository.Instance);
var dto = new UserDTO { Id = 1, Name = "Jxj" };
UserDomain user = mapper.Convert<UserDTO, UserDomain>(dto);
Assert.NotNull(user.Repository);
四、注入IOC容器的Case
- 注入IOC容器需要安裝nuget包PocoEmit.ServiceProvider
1. 包含IOC容器的實體
class UserWithServiceProvider
{
public int Id { get; set; }
public string Name { get; set; }
public IServiceProvider ServiceProvider { get; set; }
}
2. 注冊、轉化并注入的代碼
- UseSingleton是把容器作為唯一容器注入
- UseScope是使用當前Scope子容器
- UseContext是在Mvc下,使用當前HttpContext的RequestServices子容器
var services = new ServiceCollection();
var serviceProvider = services.BuildServiceProvider();
var mapper = Mapper.Create();
mapper.UseSingleton(serviceProvider);
var dto = new UserDTO { Id = 1, Name = "Jxj" };
UserWithServiceProvider user = mapper.Convert<UserDTO, UserWithServiceProvider>(dto);
Assert.NotNull(user.ServiceProvider);
五、當然還可以注入容器內的服務
1. UserDomain多出來的Repository需要注入
- 這次我們用IOC來管理Repository
- 這樣才能更好的利用依賴注入
- Repository可能還會依賴其他的服務
- 手動維護服務對象可能會很麻煩,IOC容器擅長維護這些復雜關系
class UserDomain(UserRepository repository, int id, string name)
{
private readonly UserRepository _repository = repository;
public UserRepository Repository
=> _repository;
public int Id { get; } = id;
public string Name { get; } = name;
// ...
}
class UserRepository
{
void Add(UserDomain user) { }
void Update(UserDomain entity) { }
void Remove(UserDomain entity) { }
}
2. 注冊、轉化并注入的代碼
- 通過UseScope注入IOC容器
- 通過UseDefault告知這個類型從IOC容器中注入
var services = new ServiceCollection()
.AddScoped<UserRepository>();
var serviceProvider = services.BuildServiceProvider();
var mapper = Mapper.Create();
mapper.UseScope(serviceProvider)
.UseDefault<UserRepository>();
var dto = new UserDTO { Id = 1, Name = "Jxj" };
UserDomain user = mapper.Convert<UserDTO, UserDomain>(dto);
Assert.NotNull(user.Repository);
六、支持IOC容器的特性
- 支持FromKeyedServices
- 支持FromServices
1. FromKeyedServices標記注入點和服務鍵
class UserDomain1([FromKeyedServices("User1")]UserRepository repository, int id, string name)
: UserDomain(repository, id, name)
{
}
class UserDomain2([FromKeyedServices("User2")] UserRepository repository, int id, string name)
: UserDomain(repository, id, name)
{
}
class UserDomain(UserRepository repository, int id, string name)
{
private readonly UserRepository _repository = repository;
public UserRepository Repository
=> _repository;
public int Id { get; } = id;
public string Name { get; } = name;
// ...
}
class UserRepository(string tableName)
{
private readonly string _tableName = tableName;
public string TableName
=> _tableName;
void Add(UserDomain user) { }
void Update(UserDomain entity) { }
void Remove(UserDomain entity) { }
}
2. 注冊、轉化并注入的代碼
- 由于識別出FromKeyedServices,就不需要UseDefault
- 這樣簡潔又優雅
string table1 = "User1";
string table2 = "User2";
var services = new ServiceCollection()
.AddKeyedScoped(table1, (_, _) => new UserRepository(table1))
.AddKeyedScoped(table2, (_, _) => new UserRepository(table2));
var serviceProvider = services.BuildServiceProvider();
var mapper = Mapper.Create();
mapper.UseScope(serviceProvider);
var dto = new UserDTO { Id = 1, Name = "Jxj" };
UserDomain user = mapper.Convert<UserDTO, UserDomain1>(dto);
Assert.NotNull(user.Repository);
UserDomain user2 = mapper.Convert<UserDTO, UserDomain2>(dto);
Assert.NotNull(user2.Repository);
七、競品類似的功能
1. AutoMapper不支持
- AutoMapper的NullSubstitute用來指定源屬性為null時的默認值
- 用AutoMapper實現類似功能需要復雜的自定義IValueResolver來實現
- PocoEmit在源無法匹配或源字段為null都可能觸發依賴注入
2. EF有類似功能
- 不過貌似只支持EF內部某些服務
- 請參閱 EF Core實體類的依賴注入
八、總結
1. OOM映射需要依賴注入
- DTO、實體、領域模型如果有業務邏輯就需要依賴外部服務
- 支持按類型注入,也支持按指定的參數或屬性注入
- 支持FromKeyedServices和FromServices
- 需要外部服務就需要依賴注入
2. PocoEmit的依賴注入助力程序分層架構
- 依賴注入的加持每一層想調用啥就調用啥
- 同時也讓每一層更好的劃分讓調用啥,不讓調用啥更容易控制
- 同時也讓業務需要劃分多少層就劃分多層變得簡單
3. IOC容器使用需要注意
- 簡單作業單容器,使用UseSingleton即可
- 多線程需要使用UseScope
- Mvc(含WebApi)邏輯處理使用UseContext
- UseContext需要引用nuget包PocoEmit.Mvc
- 如果是Mvc異步處理或Quartz類似作業不要用UseContext
- 就怕異步中獲取到了HttpContext,但執行中途被釋放了,后面就可能異常了
另外源碼托管地址: ,歡迎大家直接查看源碼。
gitee同步(bu)更新(xin):
如果大(da)家喜歡請動動您發(fa)財的小(xiao)手手幫忙點一(yi)下Star,謝(xie)謝(xie)!!!