Skip to content
Aaron Hanusa edited this page Feb 7, 2021 · 115 revisions

ServiceBase is one of the main actors within the Peasy Framework. A concrete implementation becomes what is called a service class, and exposes CRUD and other command methods (defined by you).

ServiceBase is responsible for exposing commands that subject data proxy operations (and other logic) to validation and business rules via the command execution pipeline before execution. The commands returned by the methods can be invoked in a thread-safe manner by multiple .NET clients. You can think of a an implementation of ServiceBase as a CRUD command factory.

Sample consumption scenario

var service = new CustomerService(new CustomerEFDataProxy());
var customer = new Customer() { Name = "Frank Zappa" };

var executionResult = await service.InsertCommand(customer).ExecuteAsync();

if (executionResult.Success)
{
   customer = executionResult.Value;
   Debug.WriteLine(customer.ID.ToString());
}
else
   Debug.WriteLine(String.Join(",", executionResult.Errors.Select(e => e.ErrorMessage)));

Public methods

GetByIDCommand

Accepts the id of a resource that you want to query and returns a constructed command. The command subjects the id to validation and business rules (if any) before marshaling the call to IDataProxy.GetByIDAsync.

The execution of the command returned by GetByIDCommand invokes the following hooks, respectively:

  1. OnGetByIDCommandInitializationAsync - the first method invoked within the command execution pipeline. Override this method to perform initialization logic before rule validations occur.

  2. OnGetByIDCommandGetRulesAsync - the second method invoked within the command execution pipeline. Override this method to configure rules whose successful validation will determine whether or not to proceed with command pipeline execution.

  3. OnGetByIDCommandValidationSuccessAsync - the final method invoked within the command execution pipeline. Override this method to invoke custom business logic and data proxy interaction. This method is only invoked based on the successful validation of all configured validation and business rules.

GetAllCommand

Returns a command that delivers all values from a data source and is especially useful for lookup data. The command executes business rules (if any) before marshaling the call to IDataProxy.GetAllAsync.

The execution of the command returned by GetAllCommand invokes the following hooks, respectively:

  1. OnGetAllCommandInitializationAsync - the first method invoked within the command execution pipeline. Override this method to perform initialization logic before rule validations occur.

  2. OnGetAllCommandGetRulesAsync - the second method invoked within the command execution pipeline. Override this method to configure rules whose successful validation will determine whether or not to proceed with command pipeline execution.

  3. OnGetAllCommandValidationSuccessAsync - the final method invoked within the command execution pipeline. Override this method to invoke custom business logic and data proxy interaction. This method is only invoked based on the successful validation of all configured validation and business rules.

InsertCommand

Accepts a DTO that you want inserted into a data store and returns a constructed command. The command subjects the DTO to validation and business rules (if any) before marshaling the call to IDataProxy.InsertAsync.

The execution of the command returned by InsertCommand invokes the following hooks, respectively:

  1. OnInsertCommandInitializationAsync - the first method invoked within the command execution pipeline. Override this method to perform initialization logic before rule validations occur.

  2. OnInsertCommandGetRulesAsync - the second method invoked within the command execution pipeline. Override this method to configure rules whose successful validation will determine whether or not to proceed with command pipeline execution.

  3. OnInsertCommandValidationSuccessAsync - the final method invoked within the command execution pipeline. Override this method to invoke custom business logic and data proxy interaction. This method is only invoked based on the successful validation of all configured validation and business rules.

UpdateCommand

Accepts a DTO that you want updated in a data store and returns a constructed command. The command subjects the DTO to validation and business rules (if any) before marshaling the call to IDataProxy.UpdateAsync.

The execution of the command returned by UpdateCommand invokes the following hooks, respectively:

  1. OnUpdateCommandInitializationAsync - the first method invoked within the command execution pipeline. Override this method to perform initialization logic before rule validations occur.

  2. OnUpdateCommandGetRulesAsync - the second method invoked within the command execution pipeline. Override this method to configure rules whose successful validation will determine whether or not to proceed with command pipeline execution.

  3. OnUpdateCommandValidationSuccessAsync - the final method invoked within the command execution pipeline. Override this method to invoke custom business logic and data proxy interaction. This method is only invoked based on the successful validation of all configured validation and business rules.

DeleteCommand

Accepts the id a resource that you want to delete from the data store and returns a constructed command. The command subjects the id to validation and business rules (if any) before marshaling the call to IDataProxy.DeleteAsync.

The execution of the command returned by DeleteCommand invokes the following hooks, respectively:

  1. OnDeleteCommandInitializationAsync - the first method invoked within the command execution pipeline. Override this method to perform initialization logic before rule validations occur.

  2. OnDeleteCommandGetRulesAsync - the second method invoked within the command execution pipeline. Override this method to configure rules whose successful validation will determine whether or not to proceed with command pipeline execution.

  3. OnDeleteCommandValidationSuccessAsync - the final method invoked within the command execution pipeline. Override this method to invoke custom business logic and data proxy interaction. This method is only invoked based on the successful validation of all configured validation and business rules.

Creating a service

To create a service, you must inherit from the abstract classes ServiceBase. There are 3 contractual obligations to fulfill when inheriting from one of these classes:

  1. Create a DTO - your DTO will define an ID property which will need to be specified as the TKey generic parameter.
  2. Create a data proxy.
  3. Create a class that inherits ServiceBase, specify the DTO (T) and ID (TKey) as the generic type arguments, respectively, and require the data proxy as a constructor argument, passing it to the base class constructor.

Here's an example:

public class CustomerService : ServiceBase<Customer, int>
{
    public CustomerService(IDataProxy<Customer, int> dataProxy) : base(dataProxy)
    {
    }
}

The CustomerService class inherits from ServiceBase, specifying the Customer DTO as the T argument and an int for the TKey argument. Specifying these values creates strongly typed command method signatures. In addition, a required constructor argument of IDataProxy<Customer, int> must be passed to the constructor of ServiceBase.

Providing initialization logic

Initialization logic can be helpful when you need to initialize a DTO with required values before it is subjected to validations or to perform other cross-cutting concerns, such as logging, instrumentation, etc.

Within the command execution pipeline, you have the opportunity to inject initialization logic that occurs before validation and business rules are executed.

Here's an example that injects initialization behavior into execution pipeline of the command returned by ServiceBase.InsertCommand in an OrderItemService. This initialization logic executes before any validation and business rules that have been wired up.

protected override Task OnInsertCommandInitializationAsync(OrderItem entity, ExecutionContext<OrderItem> context)
{
    entity.StatusID = STATUS.Pending;
    return Task.CompletedTask;
}

In this example we simply override OnInsertCommandInitializationAsync and set the default status to pending to satisfy a required field validation that may not have been set by the consumer of the application.

Overriding default command logic

By default, all service command methods of a default implementation of ServiceBase are wired up to invoke data proxy methods. There will be times when you need to invoke extra command logic before and/or after execution occurs. For example, you might want to perform logging before and after communication with a data proxy during the command's execution to obtain performance metrics for your application.

Here is an example that allows you to achieve this behavior:

protected override async Task<OrderItem> OnInsertCommandValidationSuccessAsync(OrderItem resource, ExecutionContext<OrderItem> context)
{
    await _logger.LogStartTimeAsync();
    var orderItem = await base.OnInsertCommandValidationSuccessAsync(resource, context);
    await _logger.LogEndTimeAsync();
    return orderItem;
}

In this example we override OnInsertCommandValidationSuccessAsync to subject logging functionality to the execution pipeline for ServiceBase.InsertCommand.

Exposing new command methods

There will be cases when you want to create new command methods in addition to the default command methods. For example, you might want your Orders Service to return all orders placed on a specific date. In this case, you could provide a GetOrdersPlacedOnCommand(DateTime date) or similar method.

There will also be times when you want to disallow updates to certain fields on a DTO in UpdateCommand, however, you still need to provide a way to update the field within a different context.

For example, let's suppose your OrderItem DTO exposes a Status field that you don't want updated via UpdateCommand for security or special auditing purposes, but you still need to allow Order Items to progress through states (Pending, Submitted, Shipped, etc.)

Below is how you might expose a new service command method to expose this functionality:

Exposing a command method that returns a ServiceCommand instance

public ICommand<OrderItem> SubmitCommand(long orderItemID)
{
    var proxy = DataProxy as IOrderItemDataProxy;
    return new ServiceCommand<OrderItem>
    (
        getBusinessRulesMethod: () => GetBusinessRulesForSubmitAsync(orderItemID),
        executeMethod: () => proxy.SubmitAsync(orderItemID, DateTime.Now)
    );
}

private async Task<IEnumerable<IRule>> GetBusinessRulesForSubmitAsync(long orderItemID)
{
    var orderItem = await DataProxy.GetByIDAsync(orderItemID);
    return new CanSubmitOrderItemRule(orderItem).ToArray();
}

Here we publicly expose a SubmitCommand method from our OrderItemService. Within the method implementation, we create an instance of ServiceCommand, which is a reusable command that accepts functions as parameters.

In this scenario, we use a particular ServiceCommand constructor overload that requires pointers to methods that will execute upon command invocation of the returned command.

For brevity, the proxy.SubmitAsync method has been left out, however you can imagine that the proxy will update the status for the supplied orderItemID in the backing data store.

One final note is that we have wired up business rule methods for the submit command. This means that the call to proxy.SubmitAsync will only occur if the validation result for CanSubmitOrderItemRule is successful. For more information on rule configuration, see Configuring business and validation rules.

Exposing a command method that returns a custom ICommand implementation

While you can always return a ServiceCommand instance from your service methods, sometimes you might want a command class that encapsulates the orchestration of logic that is subjected to cross-cutting concerns, such as logging, caching, and transactional support. In this case, you can create a custom command and return it from your new service command method.

Here’s an example:

public ICommand<OrderItem> ShipCommand(long orderItemID)
{
    var proxy = DataProxy as IOrderItemDataProxy;
    return new ShipOrderItemCommand(orderItemID, proxy, _inventoryService);
}

In this example, we are simply returning a new instance of ShipOrderItemCommand from the ShipCommand method. A full implementation for this command can be found here, but it should be understood that ShipOrderItemCommand implements ICommand<OrderItem>.

ExecutionContext

Often times you will need to obtain data that rules rely on for validation. This same data is often needed for service command methods that require it for various reasons. ExecutionContext is an object passed through the command execution pipeline for all default command methods and can carry with it data to be shared between function calls.

protected override async Task<IEnumerable<IRule>> OnUpdateCommandGetRulesAsync(OrderItem resource, ExecutionContext<OrderItem> context)
{
    var item = await base.DataProxy.GetByIDAsync(resource.ID);
    context.CurrentEntity = item;
    return new ValidOrderItemStatusForUpdateRule(item).ToArray();
}

protected override async Task<OrderItem> OnUpdateCommandValidationSuccessAsync(OrderItem resource, ExecutionContext<OrderItem> context)
{
    var current = context.CurrentEntity;
    resource.RevertNonEditableValues(current);
    return await base.DataProxy.UpdateAsync(resource);
}

In this example, we have overridden OnUpdateCommandGetRulesAsync to subject a rule to our command execution pipeline. We can see that the rule requires the current state of the requested order item. We also have overridden OnUpdateCommandValidationSuccessAsync to provide additional functionality and were able to share data between calls.

Manipulating validation and business rule execution

Business rule execution can be expensive, especially when rules rely on querying data proxies for validation. ServiceBase command methods are configured to execute validation and business rules before the request is marshaled to their respective data proxy methods. However, you might want skip validation of business rules altogether in the event that one or more validation rules fails.

Here's how you might do that in a CustomersService:

protected override async Task<IEnumerable<ValidationResult>> OnInsertCommandPerformValidationAsync(Customer resource, ExecutionContext<Customer> context)
{
    var validationErrors = OnInsertCommandValidateObject(resource, context);
    if (!validationErrors.Any())
    {
        var businessRules = await OnInsertCommandGetRulesAsync(resource, context);
        return validationErrors.Concat(await businessRules.ValidateAllAsync());
    }
    return validationErrors;
}

In this example, we have overridden OnInsertCommandPerformValidationAsync, which is the method that executes both validation and business rules when ServiceBase.InsertCommand is executed. But instead of always invoking all rules, we first invoke the validation rules, and if any of them fail validation, we simply return them without invoking the potentially expensive business rules.

For more information on rule configuration, see Configuring business and validation rules.