Aggregate Roots
Aggregate Roots in Arc provide automatic dependency injection and seamless integration with Chronicle’s event sourcing capabilities. The client automatically resolves aggregate roots based on the event source ID from the Command Context values provided by Resolving EventSourceId.
Overview
Section titled “Overview”Arc automatically registers all aggregate root types and provides them as transient services in the dependency injection container. When an aggregate root is requested, it uses the event source ID from the current command context to load the appropriate instance.
Automatic Registration
Section titled “Automatic Registration”Aggregate roots are automatically discovered and registered when you configure Arc.
This will scan for all aggregate root types and register them with the dependency injection container.
Taking Dependencies on Aggregate Roots
Section titled “Taking Dependencies on Aggregate Roots”You can inject aggregate roots directly into your commands through the Handle method signature:
public record AddItemToOrderCommand([Key] Guid OrderId, Guid ProductId, int Quantity, decimal Price){ public object Handle(Order order, ILogger<AddItemToOrderCommand> logger) { order.AddItem(ProductId, Quantity, Price);
// The changes are automatically tracked and will be committed // when the command handler completes successfully return new ItemAddedToOrder { ProductId = ProductId, Quantity = Quantity, Price = Price }; }}Event Source ID Resolution
Section titled “Event Source ID Resolution”The aggregate root resolution depends entirely on the event source ID being available in the command context. Resolving EventSourceId supplies this value through the Command Context Values pipeline. The resolution process works as follows:
- Command Context Lookup: The system retrieves the event source ID from the current
CommandContext - Validation: If no event source ID is found, an
UnableToResolveAggregateRootFromCommandContextexception is thrown - Factory Invocation: The
IAggregateRootFactory.Get<T>()method is called with the resolved event source ID - Instance Return: The loaded aggregate root instance is returned
Event Source ID Requirements
Section titled “Event Source ID Requirements”For aggregate root resolution to work, the command must provide an event source ID through one of these methods:
- Implement
ICanProvideEventSourceId - Have a property of type
EventSourceId - Have a property marked with
[Key]attribute - Be part of a tuple that contains an
EventSourceId
Example Usage
Section titled “Example Usage”Basic Command Handler
Section titled “Basic Command Handler”public record CreateUserCommand(EventSourceId UserId, string Email, string Name){ public UserCreated Handle(User user) { // The 'user' aggregate root is automatically loaded using the UserId // from the command as the event source ID
user.Create(Email, Name);
return new UserCreated { Email = Email, Name = Name }; }}Update Command Handler
Section titled “Update Command Handler”public record UpdateUserEmailCommand([Key] Guid UserId, string NewEmail){ public UserEmailUpdated Handle(User user) { // The 'user' aggregate root is loaded using UserId as event source ID user.UpdateEmail(NewEmail);
return new UserEmailUpdated { NewEmail = NewEmail }; }}Multiple Aggregate Roots
Section titled “Multiple Aggregate Roots”Note that with the current pattern, you can only automatically resolve one aggregate root per command (based on the event source ID). For scenarios involving multiple aggregates, you’ll need to load additional ones manually:
public record TransferFundsCommand(Guid FromAccountId, Guid ToAccountId, decimal Amount) : ICanProvideEventSourceId{ // This command uses FromAccountId as the primary event source public EventSourceId GetEventSourceId() => FromAccountId.ToString();
public FundsTransferred Handle(Account fromAccount, IAccountRepository accountRepository) { // Load the target account manually since we can only auto-resolve one var toAccount = accountRepository.GetById(ToAccountId).GetAwaiter().GetResult();
fromAccount.TransferTo(toAccount, Amount);
return new FundsTransferred { ToAccountId = ToAccountId, Amount = Amount }; }}```## Error Handling
### UnableToResolveAggregateRootFromCommandContext
This exception is thrown when:
- No event source ID is available in the command context- The event source ID is `EventSourceId.Unspecified`
```csharppublic record InvalidCommand(string SomeProperty);// No event source ID property or interface implementation
// This will fail because no event source ID can be resolvedLifecycle Management
Section titled “Lifecycle Management”Transient Scope
Section titled “Transient Scope”Aggregate roots are registered as transient services, meaning:
- A new instance is created for each request
- The instance is tied to the specific event source ID from the command context
- Changes made to the aggregate root are automatically tracked
- The aggregate root is automatically disposed after the command completes
Automatic Commit
Section titled “Automatic Commit”When using aggregate roots through dependency injection:
- Changes are automatically tracked by Chronicle’s change tracking system
- Events generated by the aggregate root are automatically committed when the command handler completes successfully
- If an exception occurs, changes are automatically rolled back
Best Practices
Section titled “Best Practices”Single Responsibility
Section titled “Single Responsibility”Keep command handlers focused on a single aggregate root when possible:
// Good: Single aggregate rootpublic record AddItemCommand([Key] Guid OrderId, Guid ProductId, int Quantity){ public object Handle(Order order) => order.AddItem(ProductId, Quantity);}
// Consider refactoring: Multiple concerns would require manual loading```### Event Source ID Clarity
Make it clear which property serves as the event source ID:
```csharp// Clear and explicitpublic record UpdateOrderCommand(EventSourceId OrderId, string Status); // Obviously the event source ID
// Also clear with Key attributepublic record UpdateOrderCommand([Key] Guid OrderId, string Status); // Marked as the keyValidation
Section titled “Validation”Validate that the event source ID is meaningful before processing:
public record UpdateOrderCommand([Key] Guid OrderId, string Status){ public object Handle(Order order) { if (order.IsDeleted) { throw new OrderAlreadyDeletedException(OrderId); }
order.UpdateStatus(Status); return new OrderUpdated { Status = Status }; }}```## Advanced Scenarios
### Custom Aggregate Root Resolution
If you need custom resolution logic, you can bypass the automatic injection and use `IAggregateRootFactory` directly:
```csharppublic record SomeCommand(string SomeProperty){ public object Handle(IAggregateRootFactory aggregateRootFactory, CommandContext commandContext) { var customEventSourceId = DetermineCustomEventSourceId(); var aggregate = aggregateRootFactory.Get<MyAggregate>(customEventSourceId).GetAwaiter().GetResult();
// Process with custom-loaded aggregate return new SomeEvent(); }
private EventSourceId DetermineCustomEventSourceId() => EventSourceId.New();}```### Conditional Aggregate Loading
```csharppublic record ConditionalCommand([Key] Guid OrderId, bool ShouldProcessOrder){ public object Handle(IServiceProvider serviceProvider) { if (ShouldProcessOrder) { // Only resolve Order when needed var order = serviceProvider.GetRequiredService<Order>(); order.Process(); return new OrderProcessed(); }
return new CommandIgnored(); }}