Domain-Driven Design is an approach to software development that suggests that (1) For most software projects, the primary focus should be on the domain and domain logic; and (2) Complex domain designs should be based on a model.
Excerpted from Domain-Driven Design Book by Eric Evans
A cluster of associated objects that are treated as a unit for the purpose of data changes. External references are restricted to one member of the AGGREGATE, designated as the root. A set of consistency rules applies within the AGGREGATE’S boundaries.
public class Account : IAggregate
{
}
or
[Aggregate]
public class Account
{
}
The delimited applicability of a particular model. BOUNDING CONTEXTS gives team members a clear and shared understanding of what has to be consistent and what can develop independently.
public class AccountManagement : IBoundedContext
{
public string Name => "AccountManagement";
}
or
[BoundedContext("AccountManagement")]
public class AccountManagement
{
}
An object fundamentally defined not by its attributes, but by a thread of continuity and identity.
public class Account : IEntity
{
public int Id { get; }
public bool Equals(Account other) => Id == other.Id;
}
or
[Entity]
public class Account
{
public int Id { get; }
public bool Equals(Account other) => Id == other.Id;
}
A mechanism for encapsulating complex creation logic and abstracting the type of a created object for the sake of a client.
public class AccountFactory : IFactory
{
public Account CreateAccount(string name) => new Account(name);
}
or
[Factory]
public class AccountFactory
{
public Account CreateAccount(string name) => new Account(name);
}
A mechanism for encapsulating storage, retrieval, and search behavior which emulates a collection of objects.
public interface IAccountRepository : IRepository<Account>
{
Account FindById(int id);
}
or
[Repository(typeof(Account))]
public interface IAccountRepository
{
Account FindById(int id);
}
An operation offered as an interface that stands alone in the model, with no encapsulated state.
public class AccountService : IService
{
public void Transfer(Account source, Account dest, Money value)
{
source.Withdraw(value);
dest.Enroll(value);
}
}
or
[Service]
public class AccountService
{
public void Transfer(Account source, Account dest, Money value)
{
source.Withdraw(value);
dest.Enroll(value);
}
}
###Value object
An object that describes some characteristic or attribute but carries no concept of identity.
public class Money : IValueObject
{
public decimal Value { get; }
public Currency Currency { get; }
public bool Equals(Money other) => Value == other.Value && Equals(Currency, other.Currency);
}
or
[ValueObject]
public class Money
{
public decimal Value { get; }
public Currency Currency { get; }
public bool Equals(Money other) => Value == other.Value && Equals(Currency, other.Currency);
}
Domain Events work in exactly the same way that an event based architecture works in other contexts.
You will typically create a new event such as UserWasRegistered. This will be a class that holds the required details of the event that just took place, in this case an instance a User object.
public class UserWasRegistered : IDomainEvent
{
public UserWasRegistered(User user)
{
User = user;
}
public User User { get; }
}
public class UserManager : IService
{
private readonly IUserRepository _userRepository;
private readonly IUserFactory _userFactory;
public async Task RegisterUserAsync(string name, CancellationToken cancellationToken)
{
User user _userFactory.CreateUser(name);
await _userRepository.AddAsync(user, cancellationToken);
UserWasRegistered event = new UserWasRegistered(user);
await DomainEvents.RiseAsync(event, cancellationToken);
}
}
Next you will write handler to handle the event. For example, you might have a handler called SendNewUserWelcomeEmail. This would be a class that accepts the UserWasRegistered event and uses the User object to send the email.
public class SendNewUserWelcomeEmail : IDomainEventHandler<UserWasRegistered>
{
private IMailService _mailService;
public async Task HandleAsync(UserWasRegistered event, CancellationToken cancellationToken)
{
MailMessage message = CreateMail(event.User);
await _mailService.SendMessageAsync(member, cancellationToken);
}
}
The SendNewUserWelcomeEmail is responsible for having the ability to send the email and so the process for registering a new user is completely decoupled from the process of sending the email.
You can also register multiple listeners for events so you can very easily add or remove actions that should be fired whenever an event takes place.
public void ConfigureDomainEvents
{
var handlers = new IDomainEventHandler[]
{
new SendNewUserWelcomeEmail(),
new CreateUserAccount()
}
DomainEvents.Dispatcher = new DomainEventsDispatcher(handlers);
}
Reliably publish events whenever state changes by using Event Sourcing. Event Sourcing persists each business entity as a sequence of events, which are replayed to reconstruct the current state.
Event sourcing persists the state of a business entity such an Order or a Customer as a sequence of state-changing events. Whenever the state of a business entity changes, a new event is appended to the list of events. Since saving an event is a single operation, it is inherently atomic. The application reconstructs an entity’s current state by replaying the events.
Applications persist events in an event store, which is a database of events. The store has an API for adding and retrieving an entity’s events. The event store also behaves like a message broker. It provides an API that enables services to subscribe to events. When a service saves an event in the event store, it is delivered to all interested subscribers.
public class RenameEvent(string name) : IEntityEvent
{
public string Name { get; } = name;
}
public class ChangeNumberEvent(string number) : IEntityEvent
{
public string Number { get; } = number;
}
public class User : Aggregate<IEntityEvent>, IEntity
{
public string Name { get; private set; }
public string Number { get; private set; }
public void Rename(string name)
{
var evnt = new RenameEvent(name);
RiseEvent(evnt);
}
public void ChangeNumber(string number)
{
var evnt = new ChangeNumberEvent(number);
RiseEvent(evnt);
}
protected override void ApplyEvent(IEntityEvent entityEvent)
{
ApplyEvent((dynamic)entityEvent);
}
private void ApplyEvent(RenameEvent evnt)
{
Name = evnt.Name;
}
private void ApplyEvent(ChangeNumberEvent evnt)
{
Number = evnt.Number;
}
}
public class UserRepository : IRepository<User>
{
private readonly IEventStore _eventStore;
public async Task<User> FindByIdAsync(int id, CancellationToken cancellationToken)
{
IDomainEvent[] events = await _eventStore.LoadUserEventsAsync(id, cancellationToken);
if (events.Length == 0)
return null;
User user = CreateEmptyUser();
user.AsAggregate().ApplyEvents(events);
return user;
}
public async Task ModifyAsync(User user, CancellationToken cancellationToken)
{
IDomainEvent[] events = user.AsAggregate().Events;
await _eventStore.PersistUserEventsAsync(user.Id, events, cancellationToken);
}
}