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

CommandBase 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. CommandBase 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

ExecuteAsync

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.

The command execution pipeline diagram can help to visualize all of this.

Creating a command derived from CommandBase

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

  1. Override OnExecuteAsync - this method is invoked asynchronously via CommandBase.ExecuteAsync and conditionally run based on the success of any configured business rule invocations.
  2. Override OnGetRulesAsync (optional) - this method can be overridden to configure business rules whose execution must pass validation in order for OnExecuteAsync to be invoked.
  3. Override OnInitializationAsync (optional) - this method can be overridden to provide initialization logic before the remainder of the command is executed.

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

ShipOrderItemCommand

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

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

    private OrderItem CurrentOrderItem { get; set; }

    protected override async Task OnInitializationAsync()
    {
        CurrentOrderItem = await _orderItemDataProxy.GetByIDAsync(_orderItemID);
    }

    protected override Task<IEnumerable<IRule>> OnGetRulesAsync()
    {
        return TheseRules
        (
            new CanShipOrderItemRule(CurrentOrderItem)
        );
    }

    protected override Task<OrderItem> OnExecuteAsync()
    {
        using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
        {
            var inventoryItem = await _inventoryDataProxy.GetByProductAsync(CurrentOrderItem.ProductID);
            if (inventoryItem.QuantityOnHand - CurrentOrderItem.Quantity >= 0)
            {
                CurrentOrderItem.OrderStatus().SetShippedState();
                CurrentOrderItem.ShippedDate = DateTime.Now;
                inventoryItem.QuantityOnHand -= CurrentOrderItem.Quantity;
                await _inventoryDataProxy.UpdateAsync(inventoryItem);
            }
            else
            {
                CurrentOrderItem.OrderStatus().SetBackorderedState();
                CurrentOrderItem.BackorderedDate = DateTime.Now;
            }
            var result = await _orderItemDataProxy.UpdateAsync(CurrentOrderItem);
            scope.Complete();
            return result;
        }
    }
}

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

First, we override the OnInitializationAsync method. Within this method, we load the order item based on the orderItemID that was supplied to the command. This item will be reused throughout the command execution pipeline.

Next, we override the OnGetRulesAsync method and return a rule that determines whether the item can be shipped. In the event that the rule fails validation, command execution will stop and return a failed ExecutionResult. Otherwise, it will continue to OnExecuteAsync.

Finally, we override the OnExecuteAsync method, where the business orchestration logic resides. 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, we wrap everything within a Transaction to ensure that the command succeeds or fails atomically.

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 a ServiceCommand.

As stated previously, CommandBase 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