Skip to content
Aaron Hanusa edited this page Feb 1, 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

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. GetByIDCommand invokes OnGetByIDCommandGetRulesAsync for business rule retrieval.

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. GetAllCommand invokes OnGetAllCommandGetRulesAsync for business rule retrieval.

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. InsertCommand invokes OnInsertCommandGetRulesAsync for business rule retrieval.

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. UpdateCommand invokes OnUpdateCommandGetRulesAsync for business rule retrieval.

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. DeleteCommand invokes OnDeleteCommandGetRulesAsync for business rule retrieval.

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.

Wiring up business rules

ServiceBase exposes commands for invoking create, retrieve, update, and delete (CRUD) operations against the injected data proxies. These operations ensure that all validation and business rules are valid before marshaling the call to their respective data proxy CRUD operations.

For example, we may want to ensure that new customers and existing customers are subjected to an age verification check before successfully persisting it into our data store entity.

Let's consume the CustomerAgeVerificationRule, here's how that looks:

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

    protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
    {
        return TheseRules
        (
            new CustomerAgeVerificationRule(resource.BirthDate)
        );
    }

    protected override Task<IEnumerable<IRule>> OnUpdateCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
    {
        return TheseRules
        (
            new CustomerAgeVerificationRule(resource.BirthDate)
        );
    }
}

In the following example, we simply override the OnInsertCommandGetRulesAsync and OnUpdateCommandGetRulesAsync methods and provide the rule(s) that we want to pass validation before marshaling the call to the data proxy.

What we've essentially done is inject business rules into the thread-safe command execution pipeline, providing clarity as to what business rules are executed for each type of CRUD operation.

Lastly, it should be noted that the use of TheseRules() is a method for convenience and readiblity only. You can return rules in any fashion you prefer.

Wiring up multiple business rules

There's really not much difference between returning one or multiple business rules.

Here's an example of configuration multiple rules:

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

    protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
    {
        return TheseRules
        (
            new CustomerAgeVerificationRule(resource.BirthDate),
            new CustomerNameRule(resource.Name)
        );
    }

    protected override Task<IEnumerable<IRule>> OnUpdateCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
    {
        return TheseRules
        (
            new CustomerAgeVerificationRule(resource.BirthDate),
            new CustomerNameRule(resource.Name)
        );
    }
}

It should be noted that the use of TheseRules() is a method for convenience and readiblity only. You can return rules in any fashion you prefer.

Wiring up business rules that consume data proxy data

Sometimes business rules require data from data proxies for validation.

Here's how that might look:

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

    protected override async Task<IEnumerable<IRule>> OnUpdateCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
    {
        var existingCustomer = await base.DataProxy.GetByIDAsync(resource.ID);
        return new IRule[]
        {
            new SomeCustomerRule(existingCustomer),
            new AnotherCustomerRule(existingCustomer)
        };
    }
}

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

Exposing a service 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.

Clone this wiki locally