Domain Modeling
Vulthil.SharedKernel provides base types for building a rich domain model following Domain-Driven Design conventions.
Primitives
| Type | Description |
|---|---|
Entity<TId> |
Base class for entities identified by a strongly-typed ID |
AggregateRoot<TId> |
Entity subclass that tracks and raises domain events |
IDomainEvent |
Marker interface for domain events |
IDomainEventHandler<T> |
Handler contract for processing domain events |
DomainException |
Base exception for domain invariant violations |
Defining an Entity
Entities are compared by identity, not by attribute values:
public sealed class UserId(Guid value)
{
public Guid Value { get; } = value;
}
public sealed class User : AggregateRoot<UserId>
{
public string Email { get; private set; }
private User(UserId id, string email) : base(id)
{
Email = email;
}
public static User Create(string email)
{
var user = new User(new UserId(Guid.NewGuid()), email);
user.Raise(new UserCreatedEvent(user.Id));
return user;
}
public void ChangeEmail(string newEmail)
{
Email = newEmail;
Raise(new UserEmailChangedEvent(Id, newEmail));
}
}
Key points:
- The constructor is private – creation goes through a factory method that enforces invariants.
Raise()records a domain event without immediately dispatching it.- Events are collected and published later (typically when the unit of work commits).
Domain Events
Domain events capture something meaningful that happened in the domain:
public sealed record UserCreatedEvent(UserId UserId) : IDomainEvent;
public sealed record UserEmailChangedEvent(UserId UserId, string NewEmail) : IDomainEvent;
Handling Domain Events
public sealed class UserCreatedEventHandler : IDomainEventHandler<UserCreatedEvent>
{
public Task HandleAsync(UserCreatedEvent domainEvent, CancellationToken cancellationToken)
{
// Send welcome email, update read model, etc.
return Task.CompletedTask;
}
}
Domain event handlers are discovered automatically when you register the application layer with AddApplication.
Domain Events and the Outbox
When Vulthil.SharedKernel.Infrastructure is configured with outbox processing, domain events raised by aggregate roots are serialised into OutboxMessage rows during SaveChangesAsync. A background service then publishes them, guaranteeing at-least-once delivery even if the process crashes after the database commit.
See Outbox Pattern for configuration details.
Protecting Invariants
Use DomainException for invariant violations that represent programming errors or impossible states rather than expected business failures:
public void Deactivate()
{
if (!IsActive)
{
throw new DomainException("User is already inactive.");
}
IsActive = false;
}
For expected business failures (e.g. "email already taken"), prefer returning a Result with an Error instead. See Result Pattern.