在工作单元模式中重用服务调用
reusing services calls in unit of work pattern
我有一个使用 WebApi、通用存储库、EF6 和工作单元模式的场景
(为了将来自多次调用的所有更改包装到同一上下文。)
管理层用于执行对不同存储库以及其他管理器的调用。
目前 Customer Manager 确实注入了回购和其他经理,例如:
public class CustomerManager {
public CustomerManager(IRepository<Customer> _customerRepository, IRepository<Order> orderRepository, IManager itemManager) {
_orderReporsitory = orderReporsitory;
_itemManager = itemManager;
_customerRepository = customerRepository;
}
public bool Save(Customer customer) {
_orderReporsitory.Find...
_itemManager.IsItemUnique(ItemId)
_customerRepository.Save(customer);
}
}
This code does not compile, for reference only.
像这样的方法
将几个存储库包装在一个工作单元下,并将所有更改一起刷新。
我的问题还涉及添加另一个管理层,也被包装在工作单元中,并且允许调用存储库和其他管理器
(因为我想重用一些管理器逻辑。就像在示例中一样,我正在重用一些 ItemManager 逻辑)
此代码
using (var uow = new UnitOfWork<CompanyContext>())
{
var catService = new Services.CategoryService(uow);
var custService = new Services.CustomerService(uow);
var cat = new Model.Category { Name = catName };
catService.Add(dep);
custService.Add(new Model.Customer { Name = custName, Category = cat });
uow.Save();
}
正在使用与我需要的类似的东西,但我也希望能够注入服务以对它们进行单元测试(而不是在我的 manager/service 方法的主体中创建实例)
执行此操作的最佳方法是什么?
谢谢
您的带有工作单元的代码片段有几个问题,例如:
- 您在该方法中显式创建和处理工作单元,迫使您将该工作单元从一个方法传递到另一个方法,并从 class 传递到 class。
- 这会导致您违反 Dependency Inversion Principle,因为您现在依赖于具体类型(
CategoryService
和 CustomerService
),这会使您的代码复杂化并使您的代码更难测试。
- 如果您需要更改工作单元的创建、管理或处置方式,则必须对整个应用程序进行彻底的更改;违反了 Open/Closed Principle.
我在this answer中更详细地表达了这些问题。
相反,我建议有一个 DbContext
,通过完整的请求共享它,并控制它在应用程序基础结构中的生命周期,而不是在整个代码库中显式地控制它。
一种非常有效的方法是将您的服务层置于通用抽象之后。虽然这个抽象的名字无关紧要,但我通常称这个抽象为'command handler:
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
关于这个抽象有一些有趣的事情:
- 抽象描述了一种服务操作或用例。
- 操作可能具有的任何参数都包含在单个消息(命令)中。
- 每个操作都有自己独特的命令class。
例如,您的 CustomerManager
可能如下所示:
[Permission(Permissions.ManageCustomerDetails)]
public class UpdateCustomerDetailsCommand {
public Guid CustomerId { get; set; }
[Required] public string FirstName { get; set; }
[Required] public string LastName { get; set; }
[ValidBirthDate] public DateTime DateOfBirth { get; set; }
}
public class UpdateCustomerDetailsCommandHandler
: ICommandHandler<UpdateCustomerDetailsCommand> {
public UpdateCustomerDetailsCommandHandler(
IRepository<Customer> _customerRepository,
IRepository<Order> orderRepository,
IManager itemManager) {
_orderReporsitory = orderReporsitory;
_itemManager = itemManager;
_customerRepository = customerRepository;
}
public void Handle(UpdateCustomerDetailsCommand command) {
var customer = _customerRepository.GetById(command.CustomerId);
customer.FirstName = command.FirstName;
customer.LastName = command.LastName;
customer.DateOfBirth = command.DateOfBirth;
}
}
这可能看起来只是一堆额外的代码,但是有了这个消息和这个通用抽象,我们就可以轻松地应用横切关注点,例如处理工作单元:
public class CommitUnitOfWorkCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand> {
private readonly IUnitOfWork unitOfWork;
private readonly ICommandHandler<TCommand> decoratee;
public CommitUnitOfWorkCommandHandlerDecorator(
IUnitOfWork unitOfWork,
ICommandHandler<TCommand> decoratee) {
this.unitOfWork = unitOfWork;
this.decoratee = decoratee;
}
public void Handle(TCommand command) {
this.decoratee.Handle(command);
this.unitOfWork.SaveChanges();
}
}
上面的 class 是一个装饰器:它既实现了 ICommandHandler<TCommand>
又包装了 ICommandHandler<TCommand>
。这允许您围绕每个命令处理程序实现包装此装饰器的实例,并允许系统透明地保存在工作单元中所做的更改,而无需任何代码段明确地执行此操作。
也可以在这里创建一个新的工作单元,但最简单的开始是让工作单元在(网络)请求期间一直存在。
然而,这个装饰器只是您可以使用装饰器做的事情的开始。例如,这将是微不足道的:
- 应用安全检查
- 执行用户输入验证
- 运行事务中的操作
- 应用死锁重试机制。
- 通过重复数据删除防止重新发布。
- 在审计跟踪中注册每个操作。
- 存储用于排队或后台处理的命令。
我有一个使用 WebApi、通用存储库、EF6 和工作单元模式的场景 (为了将来自多次调用的所有更改包装到同一上下文。)
管理层用于执行对不同存储库以及其他管理器的调用。
目前 Customer Manager 确实注入了回购和其他经理,例如:
public class CustomerManager {
public CustomerManager(IRepository<Customer> _customerRepository, IRepository<Order> orderRepository, IManager itemManager) {
_orderReporsitory = orderReporsitory;
_itemManager = itemManager;
_customerRepository = customerRepository;
}
public bool Save(Customer customer) {
_orderReporsitory.Find...
_itemManager.IsItemUnique(ItemId)
_customerRepository.Save(customer);
}
}
This code does not compile, for reference only.
像这样的方法
将几个存储库包装在一个工作单元下,并将所有更改一起刷新。
我的问题还涉及添加另一个管理层,也被包装在工作单元中,并且允许调用存储库和其他管理器 (因为我想重用一些管理器逻辑。就像在示例中一样,我正在重用一些 ItemManager 逻辑)
此代码
using (var uow = new UnitOfWork<CompanyContext>())
{
var catService = new Services.CategoryService(uow);
var custService = new Services.CustomerService(uow);
var cat = new Model.Category { Name = catName };
catService.Add(dep);
custService.Add(new Model.Customer { Name = custName, Category = cat });
uow.Save();
}
正在使用与我需要的类似的东西,但我也希望能够注入服务以对它们进行单元测试(而不是在我的 manager/service 方法的主体中创建实例)
执行此操作的最佳方法是什么?
谢谢
您的带有工作单元的代码片段有几个问题,例如:
- 您在该方法中显式创建和处理工作单元,迫使您将该工作单元从一个方法传递到另一个方法,并从 class 传递到 class。
- 这会导致您违反 Dependency Inversion Principle,因为您现在依赖于具体类型(
CategoryService
和CustomerService
),这会使您的代码复杂化并使您的代码更难测试。 - 如果您需要更改工作单元的创建、管理或处置方式,则必须对整个应用程序进行彻底的更改;违反了 Open/Closed Principle.
我在this answer中更详细地表达了这些问题。
相反,我建议有一个 DbContext
,通过完整的请求共享它,并控制它在应用程序基础结构中的生命周期,而不是在整个代码库中显式地控制它。
一种非常有效的方法是将您的服务层置于通用抽象之后。虽然这个抽象的名字无关紧要,但我通常称这个抽象为'command handler:
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
关于这个抽象有一些有趣的事情:
- 抽象描述了一种服务操作或用例。
- 操作可能具有的任何参数都包含在单个消息(命令)中。
- 每个操作都有自己独特的命令class。
例如,您的 CustomerManager
可能如下所示:
[Permission(Permissions.ManageCustomerDetails)]
public class UpdateCustomerDetailsCommand {
public Guid CustomerId { get; set; }
[Required] public string FirstName { get; set; }
[Required] public string LastName { get; set; }
[ValidBirthDate] public DateTime DateOfBirth { get; set; }
}
public class UpdateCustomerDetailsCommandHandler
: ICommandHandler<UpdateCustomerDetailsCommand> {
public UpdateCustomerDetailsCommandHandler(
IRepository<Customer> _customerRepository,
IRepository<Order> orderRepository,
IManager itemManager) {
_orderReporsitory = orderReporsitory;
_itemManager = itemManager;
_customerRepository = customerRepository;
}
public void Handle(UpdateCustomerDetailsCommand command) {
var customer = _customerRepository.GetById(command.CustomerId);
customer.FirstName = command.FirstName;
customer.LastName = command.LastName;
customer.DateOfBirth = command.DateOfBirth;
}
}
这可能看起来只是一堆额外的代码,但是有了这个消息和这个通用抽象,我们就可以轻松地应用横切关注点,例如处理工作单元:
public class CommitUnitOfWorkCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand> {
private readonly IUnitOfWork unitOfWork;
private readonly ICommandHandler<TCommand> decoratee;
public CommitUnitOfWorkCommandHandlerDecorator(
IUnitOfWork unitOfWork,
ICommandHandler<TCommand> decoratee) {
this.unitOfWork = unitOfWork;
this.decoratee = decoratee;
}
public void Handle(TCommand command) {
this.decoratee.Handle(command);
this.unitOfWork.SaveChanges();
}
}
上面的 class 是一个装饰器:它既实现了 ICommandHandler<TCommand>
又包装了 ICommandHandler<TCommand>
。这允许您围绕每个命令处理程序实现包装此装饰器的实例,并允许系统透明地保存在工作单元中所做的更改,而无需任何代码段明确地执行此操作。
也可以在这里创建一个新的工作单元,但最简单的开始是让工作单元在(网络)请求期间一直存在。
然而,这个装饰器只是您可以使用装饰器做的事情的开始。例如,这将是微不足道的:
- 应用安全检查
- 执行用户输入验证
- 运行事务中的操作
- 应用死锁重试机制。
- 通过重复数据删除防止重新发布。
- 在审计跟踪中注册每个操作。
- 存储用于排队或后台处理的命令。