-
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 in the domain:
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, any validation failures will result in a CommandValidationException
being thrown. The exception contains a ValidationReport
.
(For more information regarding the validation library used in these examples, have a look at Its.Validation.)
The ICommand
interface carries a Principal
property which is used for authorizing the command when it's applied. When a command is applied and the authorization check fails, a CommandAuthorizationException
is thrown. Denying authorization is the default behavior of all commands in the system, until you provide authorization rules for your commands via one of several approaches.
How the ICommand.Principal
is actually evaluated is quite flexible, since it may make sense at times to keep the authorization rules with the command class, and at other times to define an application-level policy.
If your command inherits Command<T>
, you can override the Authorize
method. This provides the most granularity.
At the other end of the spectrum, you can set up authorization for all Command<T>
-derived commands for an aggregate by setting the static Command<T>.AuthorizeDefault
delegate:
Command<BankAccount>.AuthorizeDefault = (account, command) => account.Principal.IsAuthorizedTo(command);
(This is actually the default implementation, but you can use this to look up your authorization rules in whatever way you need.)
If you're testing and you want to disable all authorization checks for commands inheriting Command<T>
, you can do this:
Command<BankAccount>.AuthorizeDefault = (account, command) => true;
So what's that IsAuthorizedTo
method above? This is an extension method that provides access to Its.Domain
's authorization API, which looks like this:
AuthorizationFor<Customer>
.ToApply<Cancel>
.ToA<Order>
.Requires((customer, cancel, order) => order.CustomerId == customer.Id);
AuthorizationFor<Customer>
.ToApply<SuspendAccount>
.ToA<CustomerAccount>
.IsDenied();
You can use this API to create authorization policies and keep the complete set of authorization rules together in one place. This may be an organizational preference.
-
Reservation service: unique values and pools of reserved values
-
Conventions, type discovery, and dependency injection