Skip to content
Aaron Hanusa edited this page Feb 1, 2021 · 10 revisions

Command is the actor responsible for orchestrating the execution of initialization logic, validation/business rule execution, and command logic (data proxy invocations, workflow logic, etc.), respectively. Command implements ICommand, and custom implementations can be consumed directly or exposed via custom service command methods of your service class implementations.

A command can be executed synchronously or asynchronously and returns an ExecutionResult. The ExecutionResult simply contains a success status, a list of potential validation errors, and optionally, a value that is the result from the command execution.

Public methods

Synchronously executes initialization logic, validation/business rule execution, and command logic (data proxy invocations, workflow logic, etc.).

Asynchronously executes initialization logic, validation/business rule execution, and command logic (data proxy invocations, workflow logic, etc.).

Command execution pipeline

During command execution, initialization logic, validation and business rules, and command logic (data proxy invocations, workflow logic, etc.) is executed in a specific order (or sometimes not at all). Through protected members of ServiceBase, you have the opportunity to manipulate the execution pipeline by adding initialization logic, wiring up business rules, and overriding default command logic.

This execution can occur synchronously or asynchronously. Viewing the sequences for the synchronous pipeline and asynchronous pipeline can help to visualize all of this.

Creating a command derived from Command

The easiest way to create a custom command is to inherit from Command or Command<T>.

Inheriting from Command returns void on synchronous execution and Task on asynchronous execution via ExecutionResult.

Inheriting from Command<T> returns the object specified by T on synchronous execution and the object wrapped in a Task on asynchronous execution via ExecutionResult.

Next decide whether you want to support synchronous behavior, asynchronous behavior, or both. For each command execution pipeline, there is 1 contractual and 1 optional obligation that must be made when inheriting from Command:

Synchronous Support:

  1. Override OnExecute - this method is invoked synchronously via Command.Execute and conditionally invoked based on the absence of any errors returned from GetErrors.
  2. Override GetErrors(optional) - this method can be overridden to execute custom business rules before OnExecute is invoked.

Asynchronous Support:

  1. Override OnExecuteAsync - this method is invoked asynchronously via Command.ExecuteAsync and conditionally invoked based on the absence of any errors returned from GetErrorsAsync.
  2. Override GetErrorsAsync(optional) - this method can be overridden to execute custom business rules before OnExecuteAsync is invoked.

Here is a custom command that orchestrates the shipping of an order item:

public class ShipOrderItemCommand : Command<OrderItem>
{
    private IOrderItemDataProxy _orderItemDataProxy;
    private long _orderItemID;
    private ITransactionContext _transactionContext;
    private IInventoryItemDataProxy _inventoryDataProxy;

    public ShipOrderItemCommand(long orderItemID, IOrderItemDataProxy orderItemDataProxy, IInventoryItemDataProxy inventoryDataProxy, ITransactionContext transactionContext)
    {
        _orderItemID = orderItemID;
        _orderItemDataProxy = orderItemDataProxy;
        _inventoryDataProxy = inventoryDataProxy;
        _transactionContext = transactionContext;
    }

    private OrderItem CurrentOrderItem { get; set; }

    protected override OrderItem OnExecute()
    {
        return _transactionContext.Execute(() =>
        {
            var inventoryItem = _inventoryDataProxy.GetByProduct(CurrentOrderItem.ProductID);
            if (inventoryItem.QuantityOnHand - CurrentOrderItem.Quantity >= 0)
            {
                CurrentOrderItem.OrderStatus().SetShippedState();
                CurrentOrderItem.ShippedDate = DateTime.Now;
                inventoryItem.QuantityOnHand -= CurrentOrderItem.Quantity;
                _inventoryDataProxy.Update(inventoryItem);
            }
            else
            {
                CurrentOrderItem.OrderStatus().SetBackorderedState();
                CurrentOrderItem.BackorderedDate = DateTime.Now;
            }
            return _orderItemDataProxy.Update(CurrentOrderItem);
        });
    }

    public IEnumerable<IRule> GetRules()
    {
        yield return new CanShipOrderItemRule(CurrentOrderItem);
    }

    public override IEnumerable<ValidationResult> GetErrors()
    {
        CurrentOrderItem = _orderItemDataProxy.GetByID(_orderItemID);
        foreach (var error in GetRules().GetValidationResults())
            yield return error;
    }
}

In this example, the ShipOrderItemCommand is responsible for a few things.

As part of our contractual agreement, we override the OnExecute method. Within this method, we orchestrate decrementing inventory via the inventory data proxy and updating the status of the order item. In the event that there is insufficient inventory, we set the state of the order item to BackOrder, otherwise we set it to Shipped. Lastly, to provide some fault tolerance around this orchestration, we subject the code to the ITransactionContext.Execute method.

Note that our InventoryItem DTO (retrieved via _inventoryDataProxy.GetByProduct) implements IVersionContainer to support concurrency handling. In the event that the retrieved inventory item changes between the time it was retrieved and the time in which it was updated, the data proxy will not be able to locate that item on update and an exception will be raised. It should also be noted that in this case, it is the responsibility of the inventory data proxy to attempt to update the supplied DTO based on the DTO.ID and DTO.Version values, as is done in this example.

We also override the GetErrors method, and returning a rule specific to the current order item.

Note: For brevity, the ShipOrderItemCommand supports the synchronous pipeline only. You can see a full synchronous and asynchronous implementation of ShipOrderItemCommand here.

While this example is a bit busy, hopefully it provides a better picture of when you'd want to create your own custom command over using the more convenient ServiceCommand.

As stated previously, Command has a very specific orchestration workflow with hooks that give you opportunities to inject logic and or business rules before actual command logic execution. However, sometimes you may want to define your own execution workflow that provides more granular orchestration steps. In this case, you can create a custom command that implements ICommand directly.

Clone this wiki locally