Search code examples
c#.netdomain-driven-designcqrs

Calling two different Aggregates from inside a Single CQRS Command


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($"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; }
    }
}

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?


Solution

  • 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).