从单个 CQRS 命令中调用两个不同的聚合
Calling two different Aggregates from inside a Single CQRS Command
我正在开发在线支持票务系统。在这个系统中,不同的客户可以注册和 post 票(每张票将链接到一个客户)。为了我的问题的简单性,我打算在系统中只保留 2 个聚合,CustomerAggregate 和 TicketAggregate。这 2 个聚合的我的域模型如下所示
/Domain/Entities/CustomerAggregate/Customer.cs
namespace MyApp.Domain.Entities.CustomerAggregate
{
public class Customer : Entity, IAggregateRoot
{
public Customer(string name, int typeId)
{
Name = name;
TypeId = typeId;
}
public string Name { get; private set; }
public int TypeId { get; private set; }
public CustomerType Type { get; private set; }
}
}
/Domain/Entities/CustomerAggregate/CustomerType.cs
namespace MyApp.Domain.Entities.CustomerAggregate
{
public class CustomerType : Enumeration
{
public static CustomerType Standard = new(1, nameof(Standard));
public static CustomerType Premium = new(2, nameof(Premium));
public CustomerType(int id, string name) : base(id, name)
{
}
public static IEnumerable<CustomerType> List() =>
new[] { Standard, Premium };
public static CustomerType FromName(string name)
{
var state = List()
.SingleOrDefault(s => string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (state == null)
{
throw new MyAppDomainException($"Possible values for CustomerType: {string.Join(",", List().Select(s => s.Name))}");
}
return state;
}
public static CustomerType From(int id)
{
var state = List().SingleOrDefault(s => s.Id == id);
if (state == null)
{
throw new MyAppDomainException($"Possible values for CustomerType: {string.Join(",", List().Select(s => s.Name))}");
}
return state;
}
}
}
/Domain/Entities/TicketAggregate/Ticket.cs
namespace MyApp.Domain.Entities.Ticket
{
public class Ticket : Entity, IAggregateRoot
{
public Ticket(int customerId, string description)
{
CustomerId = customerId;
Description = description;
}
public int CustomerId { get; private set; }
public string Description { get; private set; }
}
}
在我的应用层中,我有不同的用例。例如,我有 CreateTicketCommand 基本上可以创建支持票证。我的代码如下所示
/Application/UseCases/Tickets/CreateTicketCommand.cs
namespace ConsoleApp1.Application.UseCases.Tickets.CreateTicket
{
public class CreateTicketCommand : IRequest<int>
{
public int CustomerId { get; set; }
public string Description { get; set; }
}
}
/Application/UseCases/Tickets/CreateTicketCommandHandler.cs
namespace MyApp.Application.UseCases.Tickets.CreateTicket
{
public class CreateTicketCommandHandler : IRequestHandler<CreateTicketCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTicketCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateTicketCommand command, CancellationToken cancellationToken)
{
// Is it OK to fetch Customer Entity (that belongs to different aggregate) inside a Command Handler thats basically is dealing
// with another agreegate (Ticket)
var customer = await _context.Customers.SingleOrDefaultAsync(c => c.Id = command.CustomerId);
if (customer == null)
{
throw new NotFoundException(nameof(Customer), command.CustomerId);
}
if (customer.CustomerType == CustomerType.Premium)
{
var ticket = new Ticket(command.CustomerId, command.Description);
await _context.Tickets.AddAsync(ticket, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return ticket.Id;
}
else
{
throw new InvalidOperationException();
}
}
}
}
现在我们的一项业务要求是只有高级客户才能创建票证。如果您注意到在 CreateTicketCommandHandler 内部,我首先获取 Customer,并且仅在请求的 CustomerType 为 Premium 时才创建 Ticket。
我的问题是,从单个 Command/Service(在此示例中为 Customer 和 Ticket)与多个 Aggreegates 进行交互是好的做法,还是我应该在其他地方执行此逻辑来检查 CustomerType?
更新:
我考虑的替代解决方案之一是为 CustomerType 创建一个 DomainService。
/Application/UseCases/Customers/DomainServices/CustomerTypeService.cs
public class CustomerTypeService : ICustomerTypeService
{
private IApplicationDbContext _context;
public CustomerTypeService(IApplicationDbContext context)
{
_context = context;
}
public CustomerType GetType(int customerId)
{
var customer = _context.Customer.SingleOrDefaultAsync(c => c.Id = customerId);
return customer.Type;
}
}
接口 ICustomerTypeService 将存在于工单域模型中。
/Domain/Entities/TicketAggregate/ICustomerTypeService.cs
然后在 Ticket 实体中注入 ICustomerTypeService。
public Ticket(int customerId, string description, ICustomerTypeService service)
{
var customerType = service.GetType(customerId);
//Check if customerType is valid to perform this operation, else throw exception
CustomerId = customerId;
Description = description;
}
那么在这个用例中,将客户类型逻辑放在命令处理程序中是正确的方法吗?或域服务是正确的方法?或者还有其他处理这个用例的方法吗?
“限制自己每次事务更改一个聚合”的经验法则适用于更改,不适用于访问聚合。所以你在处理一个命令的时候加载多个聚合是完全没问题的。
关于你的第二个问题,最好将所有业务逻辑都包含在你的聚合中。理想情况下,命令处理程序应该只负责接收命令、加载适当的聚合、调用指定的聚合方法以及最终持久化聚合。
考虑一个直接处理领域模型的系统。例如,您正在为 back-populate 一些数据编写脚本。您可以编写一个新命令并对其进行处理,但您也可以简单地加载聚合(或初始化新聚合)并持久化。即使在这种情况下,您也希望域规则生效并确保您的不变量得到满足。
具体来说,Ticket
可以接收客户类型值(不是客户本身)作为其构造函数(或工厂方法)的输入。
我正在开发在线支持票务系统。在这个系统中,不同的客户可以注册和 post 票(每张票将链接到一个客户)。为了我的问题的简单性,我打算在系统中只保留 2 个聚合,CustomerAggregate 和 TicketAggregate。这 2 个聚合的我的域模型如下所示
/Domain/Entities/CustomerAggregate/Customer.cs
namespace MyApp.Domain.Entities.CustomerAggregate
{
public class Customer : Entity, IAggregateRoot
{
public Customer(string name, int typeId)
{
Name = name;
TypeId = typeId;
}
public string Name { get; private set; }
public int TypeId { get; private set; }
public CustomerType Type { get; private set; }
}
}
/Domain/Entities/CustomerAggregate/CustomerType.cs
namespace MyApp.Domain.Entities.CustomerAggregate
{
public class CustomerType : Enumeration
{
public static CustomerType Standard = new(1, nameof(Standard));
public static CustomerType Premium = new(2, nameof(Premium));
public CustomerType(int id, string name) : base(id, name)
{
}
public static IEnumerable<CustomerType> List() =>
new[] { Standard, Premium };
public static CustomerType FromName(string name)
{
var state = List()
.SingleOrDefault(s => string.Equals(s.Name, name, StringComparison.CurrentCultureIgnoreCase));
if (state == null)
{
throw new MyAppDomainException($"Possible values for CustomerType: {string.Join(",", List().Select(s => s.Name))}");
}
return state;
}
public static CustomerType From(int id)
{
var state = List().SingleOrDefault(s => s.Id == id);
if (state == null)
{
throw new MyAppDomainException($"Possible values for CustomerType: {string.Join(",", List().Select(s => s.Name))}");
}
return state;
}
}
}
/Domain/Entities/TicketAggregate/Ticket.cs
namespace MyApp.Domain.Entities.Ticket
{
public class Ticket : Entity, IAggregateRoot
{
public Ticket(int customerId, string description)
{
CustomerId = customerId;
Description = description;
}
public int CustomerId { get; private set; }
public string Description { get; private set; }
}
}
在我的应用层中,我有不同的用例。例如,我有 CreateTicketCommand 基本上可以创建支持票证。我的代码如下所示
/Application/UseCases/Tickets/CreateTicketCommand.cs
namespace ConsoleApp1.Application.UseCases.Tickets.CreateTicket
{
public class CreateTicketCommand : IRequest<int>
{
public int CustomerId { get; set; }
public string Description { get; set; }
}
}
/Application/UseCases/Tickets/CreateTicketCommandHandler.cs
namespace MyApp.Application.UseCases.Tickets.CreateTicket
{
public class CreateTicketCommandHandler : IRequestHandler<CreateTicketCommand, int>
{
private readonly IApplicationDbContext _context;
public CreateTicketCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<int> Handle(CreateTicketCommand command, CancellationToken cancellationToken)
{
// Is it OK to fetch Customer Entity (that belongs to different aggregate) inside a Command Handler thats basically is dealing
// with another agreegate (Ticket)
var customer = await _context.Customers.SingleOrDefaultAsync(c => c.Id = command.CustomerId);
if (customer == null)
{
throw new NotFoundException(nameof(Customer), command.CustomerId);
}
if (customer.CustomerType == CustomerType.Premium)
{
var ticket = new Ticket(command.CustomerId, command.Description);
await _context.Tickets.AddAsync(ticket, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return ticket.Id;
}
else
{
throw new InvalidOperationException();
}
}
}
}
现在我们的一项业务要求是只有高级客户才能创建票证。如果您注意到在 CreateTicketCommandHandler 内部,我首先获取 Customer,并且仅在请求的 CustomerType 为 Premium 时才创建 Ticket。
我的问题是,从单个 Command/Service(在此示例中为 Customer 和 Ticket)与多个 Aggreegates 进行交互是好的做法,还是我应该在其他地方执行此逻辑来检查 CustomerType?
更新:
我考虑的替代解决方案之一是为 CustomerType 创建一个 DomainService。
/Application/UseCases/Customers/DomainServices/CustomerTypeService.cs
public class CustomerTypeService : ICustomerTypeService
{
private IApplicationDbContext _context;
public CustomerTypeService(IApplicationDbContext context)
{
_context = context;
}
public CustomerType GetType(int customerId)
{
var customer = _context.Customer.SingleOrDefaultAsync(c => c.Id = customerId);
return customer.Type;
}
}
接口 ICustomerTypeService 将存在于工单域模型中。
/Domain/Entities/TicketAggregate/ICustomerTypeService.cs
然后在 Ticket 实体中注入 ICustomerTypeService。
public Ticket(int customerId, string description, ICustomerTypeService service)
{
var customerType = service.GetType(customerId);
//Check if customerType is valid to perform this operation, else throw exception
CustomerId = customerId;
Description = description;
}
那么在这个用例中,将客户类型逻辑放在命令处理程序中是正确的方法吗?或域服务是正确的方法?或者还有其他处理这个用例的方法吗?
“限制自己每次事务更改一个聚合”的经验法则适用于更改,不适用于访问聚合。所以你在处理一个命令的时候加载多个聚合是完全没问题的。
关于你的第二个问题,最好将所有业务逻辑都包含在你的聚合中。理想情况下,命令处理程序应该只负责接收命令、加载适当的聚合、调用指定的聚合方法以及最终持久化聚合。
考虑一个直接处理领域模型的系统。例如,您正在为 back-populate 一些数据编写脚本。您可以编写一个新命令并对其进行处理,但您也可以简单地加载聚合(或初始化新聚合)并持久化。即使在这种情况下,您也希望域规则生效并确保您的不变量得到满足。
具体来说,Ticket
可以接收客户类型值(不是客户本身)作为其构造函数(或工厂方法)的输入。