Skip to content
ahanusa edited this page Jul 29, 2018 · 115 revisions

ServiceBase is the main actor 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 consumed synchronously or asynchronously 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" };

// Synchronous Execution
var executionResult = service.InsertCommand(customer).Execute();

// OR

// Asynchronous Execution
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 the entity 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.GetByID or IDataProxy.GetByIDAsync. GetByIDCommand invokes GetBusinessRulesForGetByID or GetBusinessRulesForGetByIDAsync 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.GetAll or IDataProxy.GetAllAsync. GetAllCommand invokes GetBusinessRulesForGetAll or GetBusinessRulesForGetAllAsync 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.Insert or IDataProxy.InsertAsync. InsertCommand invokes GetBusinessRulesForInsert or GetBusinessRulesForInsertAsync 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.Update or IDataProxy.UpdateAsync. UpdateCommand invokes GetBusinessRulesForUpdate or GetBusinessRulesForUpdateAsync for business rule retrieval.

Accepts the id of the entity 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.Delete or IDataProxy.DeleteAsync. DeleteCommand invokes GetBusinessRulesForDelete or GetBusinessRulesForDeleteAsync for business rule retrieval.

Creating a business service

To create a business service, you must inherit from the abstract classes ServiceBase or BusinessServiceBase. 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 that implements IDataProxy<T, TKey> or by creating a data proxy that inherits from one of these classes.
  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 and BusinessServiceBase expose 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)
    {
    }

    // Synchronous support
    protected override IEnumerable<IRule> GetBusinessRulesForInsert(Customer entity, ExecutionContext<Customer> context)
    {
        yield return new CustomerAgeVerificationRule(entity.BirthDate);
    }

    // Synchronous support
    protected override IEnumerable<IRule> GetBusinessRulesForUpdate(Customer entity, ExecutionContext<Customer> context)
    {
        yield return new CustomerAgeVerificationRule(entity.BirthDate);
    }

    // Asynchronous support
    protected override async Task<IEnumerable<IRule>> GetBusinessRulesForInsertAsync(Customer entity, ExecutionContext<Customer> context)
    {
        return new CustomerAgeVerificationRule(entity.BirthDate).ToArray();
    }

    // Asynchronous support
    protected override async Task<IEnumerable<IRule>> GetBusinessRulesForUpdateAsync(Customer entity, ExecutionContext<Customer> context)
    {
        return new CustomerAgeVerificationRule(entity.BirthDate).ToArray();
    }
}

In the following example, we simply override the GetBusinessRulesForInsert and GetBusinessRulesForUpdate 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 [service command] (https://github.com/peasy/Peasy.NET/wiki/ServiceCommand) synchronous execution pipeline, providing clarity as to what business rules are executed for each type of CRUD operation.

Also note that we have overridden GetBusinessRulesForInsertAsync and GetBusinessRulesForUpdateAsync. In this example, these async methods don't acquire any data asynchronously to pass to the rules as they do here, however, it is important that these be wired up when you want your rules to be subjected to the asynchronous pipeline.

Wiring up multiple business rules

There's really not much difference between returning one or multiple business rules. Because the GetBusinessRulesForCRUD methods return IEnumerable<IRule>, simply return rules sequentially via yield return, or construct an enumerable source of IRule (note that RuleBase implements IRule) and return them all at once.

Here's how that looks:

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

    // Synchronous support
    protected override IEnumerable<IRule> GetBusinessRulesForInsert(Customer entity, ExecutionContext<Customer> context)
    {
        yield return new CustomerAgeVerificationRule(entity.BirthDate);
        yield return new CustomerNameRule(entity.Name);
    }

    // Asynchronous support
    protected override async Task<IEnumerable<IRule>> GetBusinessRulesForInsertAsync(Customer entity, ExecutionContext<Customer> context)
    {
        return new []
        {
            new CustomerAgeVerificationRule(entity.BirthDate),
            new CustomerNameRule(entity.Name)
        };
    }
}

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)
    {
    }

    // Synchronous Support
    protected override IEnumerable<IRule> GetBusinessRulesForUpdate(Customer entity, ExecutionContext<Customer> context)
    {
        var existingCustomer = base.DataProxy.GetByID(entity.ID);
        yield return new SomeCustomerRule(existingCustomer);
        yield return new AnotherCustomerRule(existingCustomer);
    }

    // Asynchronous Support
    protected override async Task<IEnumerable<IRule>> GetBusinessRulesForUpdateAsync(Customer entity, ExecutionContext<Customer> context)
    {
        var existingCustomer = await base.DataProxy.GetByIDAsync(entity.ID);
        return new []
        {
            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 the synchronous command 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 void OnInsertCommandInitialization(OrderItem entity, ExecutionContext<OrderItem> context)
{
    entity.StatusID = STATUS.Pending;
}

In this example we simply override ServiceBase.OnInsertCommandInitialization 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. Note that to subject this logic to the asynchronous command execution pipeline, we would also have to override ServiceBase.OnInsertCommandInitializationAsync and provide the same logic.

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 OrderItem Insert(OrderItem entity, ExecutionContext<OrderItem> context)
{
    _logger.LogStartTime();
    var orderItem = base.Insert(entity, context);
    _logger.LogEndTime();
    return orderItem;
}

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

In this example we override ServiceBase.Insert and ServiceBase.InsertAsync to subject logging functionality to the synchronous and asynchronous execution pipelines 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) 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>
    (
        executeMethod: () => proxy.Submit(orderItemID, DateTime.Now),
        executeAsyncMethod: () => proxy.SubmitAsync(orderItemID, DateTime.Now),
        getBusinessRulesMethod: () => GetBusinessRulesForSubmit(orderItemID),
        getBusinessRulesAsyncMethod: () => GetBusinessRulesForSubmitAsync(orderItemID)
    );
}

private IEnumerable<IRule> GetBusinessRulesForSubmit(long orderItemID)
{
    var orderItem = DataProxy.GetByID(orderItemID);
    yield return new CanSubmitOrderItemRule(orderItem);
}

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 synchronous and asynchronous execution of the returned command. These methods are proxy.Submit and proxy.SubmitAsync, respectively.

For brevity, the proxy.Submit methods have 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.Submit and 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 your service methods, sometimes you might want a command class that encapsulates the orchestration of objects that are 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, _transactionContext);
}

In this example, we are simply returning ShipOrderItemCommand from the 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 actor who is passed through the command execution pipeline via ServiceBase, and can carry with it data to be shared between functions throughout the command execution workflow.

Here's what this might look like in an OrderItemService:

protected override IEnumerable<IRule> GetBusinessRulesForUpdate(OrderItem entity, ExecutionContext<OrderItem> context)
{
    var item = base.DataProxy.GetByID(entity.ID);
    context.CurrentEntity = item;
    yield return new ValidOrderItemStatusForUpdateRule(item);
}

protected override OrderItem Update(OrderItem entity, ExecutionContext<OrderItem> context)
{
    var current = context.CurrentEntity;
    entity.RevertNonEditableValues(current);
    return base.DataProxy.Update(entity);
}

In this example, we have overridden ServiceBase.GetBusinessRulesForUpdate to subject a rule to our synchronous command execution pipeline. We can see that the rule requires the current state of the requested order item. We also have overridden ServiceBase.Update (invoked by ServiceBase.UpdateCommand to supply additional functionality that occurs on update).

There are two things to notice here. First we retrieve an order item from our data proxy and assign to [ExecutionContext.CurrentEntity] (https://github.com/peasy/Peasy.NET/blob/master/src/Peasy/Peasy/ExecutionContext.cs#L7) in our overridden GetBusinessRulesForUpdate method. In addition, we retrieve the order item from the execution context in our overridden ServiceBase.Update method. In this example, the execution context allowed us to minimize our hits to the data store by sharing state between methods in the command execution pipeline.

In addition to ExecutionContext.CurrentEntity, the execution context exposes a Data property of type IDictionary<string, object> which allows the sharing of any kind of data between methods in the command execution pipeline.

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 IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> GetAllErrorsForInsert(Customer entity, ExecutionContext<Customer> context)
{
    var validationErrors = GetValidationResultsForInsert(entity, context);
    if (!validationErrors.Any())
    {
        var businessRuleErrors = GetBusinessRulesForInsert(entity, context).GetValidationResults();
        validationErrors.Concat(businessRuleErrors);
    }
    return validationErrors;
}

In this example, we have overridden ServiceBase.GetAllErrorsForInsert, 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