-
Notifications
You must be signed in to change notification settings - Fork 21
Commands
Every transition in a process modeled using an aggregate starts with a command being sent to an aggregate. The command models the intent for the change:
var withdrawFunds = new WithdrawFunds {
Amount = 10.00m
};
account.Apply(withdrawFunds);
Its.Cqrs uses command classes (deriving from ICommand<T>
or Command<T>
) instead of methods to expose ways to change the aggregate. This allows a more robust model for authorization, validation, and serializability.
The primary responsibility of a command class is to perform validation checks, including validation of business rules. When a command is applied to an aggregate, a number of checks are performed:
-
Idemptotency: Has this exact command instance been applied to the aggregate in the past?
-
Authorization: Is the sender of the command authorized, as defined by the domain, to perform this command?
-
Applicability to the current version of the aggregate: By setting
Command<T>.AppliesToVersion
a command can be pre-emptively invalidated by changes to the aggregate that may happen before the command is applied. (This is separate from the usual concurrency control provided by the event store, and is mostly used when scheduling commands for future delivery.) -
Self-validation:
Command<T>.CommandValidator
provides a way for a command class to expose validation of its own state, analagous to input or parameter validation. This check is generally performed before an aggregate is sourced from the event store. Here's an example fromSample.Banking.Domain.WithdrawFunds
:
public override IValidationRule CommandValidator
{
get
{
return Validate.That<WithdrawFunds>(cmd => cmd.Amount > 0)
.WithErrorMessage("You cannot make a withdrawal for a negative amount.");
}
}
-
Validation against the aggregate:
ICommand<T>.Validator
provides a second validator that allows the command to be validated against the state of the aggregate once it has been sourced from the event store. Here's an example:
public override IValidationRule<CheckingAccount> Validator
{
get
{
var accountIsNotClosed =
Validate.That<CheckingAccount>(account => account.DateClosed == null)
.WithErrorMessage("You cannot make a withdrawal from a closed account.");
var fundsAreAvailable = Validate.That<CheckingAccount>(account => account.Balance >= Amount);
return new ValidationPlan<CheckingAccount>
{
accountIsNotClosed,
fundsAreAvailable.When(accountIsNotClosed)
};
}
}
It's worth noting that these validators are exposed as properties so that you can do a validation check without actually applying a command, for example to provide feedback via a UI or API about what actions are allowed or what parameters are incorrect:
var validationReport = account.Validate(withdrawFunds);
The ValidationReport
allows you to see details about all of the failed validation rules and use that information as needed.
When applying the command, however, validation failures result in a CommandValidationException
, which contains a ValidationReport
.
(For more information regarding the validation library used in these examples, have a look at Its.Validation.)
-
Reservation service: unique values and pools of reserved values
-
Conventions, type discovery, and dependency injection