从单个 CQRS 命令内部调用两个不同的聚合
我正在开发在线支持票务系统。在这个系统中,不同的客户可以注册并发布票证(每张票证将链接到一个客户)。为了简单起见,我将在系统中仅保留 2 个聚合:CustomerAggregate 和 TicketAggregate。这两个聚合的我的域模型如下所示
/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 时才创建票证。
我的问题是,从单个命令/服务(在本例中为客户和票证)与多个聚合进行交互是一种好的做法,还是我应该在其他地方执行此逻辑来检查 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
然后将 ICustomerTypeService 注入到 Ticket 实体中。
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;
}
那么在这个用例中,将客户类型逻辑放入命令处理程序中是正确的方法吗?或者域服务是正确的方法吗?或者还有其他方法可以处理这个用例吗?
I am working on a Online Support Ticketing System. In this system, different customers can register and post tickets (Each ticket will be linked to a customer). For the simplicity of my question I am going to keep only 2 Aggregates in the System, CustomerAggregate and TicketAggregate. My Domain model for those 2 Aggregates look as follows
/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(quot;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(quot;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; }
}
}
Inside my Application layer, I have different use cases. For example, I have CreateTicketCommand that basically creates the support ticket. My code looks as follows
/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();
}
}
}
}
Now one of our Business requirement is that only Premium Customer can create a Ticket. If you notice that inside CreateTicketCommandHandler, I am first fetching the Customer and only creating the Ticket if the requested CustomerType is Premium.
My question here is, is it good practice to interact with multiple Aggreegates from a single Command/Service (in this example Customer and Ticket), or should I be doing this logic to check CustomerType somewhere else?
Updated:
One of the alternate solution I was thinking was to create a DomainService for CustomerType.
/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;
}
}
The interface ICustomerTypeService will exist inside Ticket Domain Model.
/Domain/Entities/TicketAggregate/ICustomerTypeService.cs
And then inject ICustomerTypeService inside Ticket entity.
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;
}
So in this usecase, putting that customertype logic inside command handler is right approach? or Domain service is right approach? or is there any other way this usecase should be handled?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
“限制自己在每笔交易中更改一个聚合”的经验法则适用于更改,而不是访问聚合。因此,在处理命令时加载多个聚合是完全可以的。
至于你的第二个问题,最好将所有业务逻辑都包含在聚合中。理想情况下,命令处理程序应该只负责接收命令、加载适当的聚合、调用指定的聚合方法,最后保存聚合。
想象一个直接处理领域模型的系统。例如,您正在编写一个脚本来回填一些数据。您可以编写一个新命令并处理它,但您也可以简单地加载聚合(或初始化新聚合)并保留。即使在这种情况下,您也希望域规则生效并确保您的不变量保持满足。
具体来说,
Ticket
可以接收客户类型值(不是客户本身)作为其构造函数(或工厂方法)的输入。The thumb rule of "restrict yourself to change one aggregate per transaction" applies to changes, not accessing aggregates. So it is completely fine for you to load multiple aggregates when processing a command.
As for your second question, it is better to have all business logic within your aggregate. Command Handlers should ideally only be responsible for receiving commands, loading the appropriate aggregate, invoking the designated aggregate method, and finally persisting the aggregate.
Think of a system where you are directly dealing with the domain model. For example, you are writing a script to back-populate some data. You could write a new command and process it, but you could also simply load the aggregates (or initialize new ones) and persist. Even in this case, you want the domain rules to kick in and ensure your invariants remain satisfied.
Specifically,
Ticket
can receive the customer type value (not the customer itself) as input to its constructor (or factory method).