-
Notifications
You must be signed in to change notification settings - Fork 68
ServiceBase
ServiceBase is the main actor within the core 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] (https://github.com/ahanusa/Peasy.NET/wiki/ServiceCommand) 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
- Public methods
- Creating a business service
- Wiring up business rules
- Wiring up multiple business rules
- Wiring up business rules that consume data proxy data
- Providing initialization logic
- Overriding default command logic
- Exposing new service command methods
- Exposing a service command method that returns a ServiceCommand instance
- Exposing a service command method that returns a custom ICommand implementation
- ExecutionContext
- Manipulating validation and business rule execution
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)));
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.
To create a business service, you must inherit from the abstract classes Peasy.Core.ServiceBase or Peasy.BusinessServiceBase. There are 3 contractual obligations to fulfill when inheriting from one of these classes:
- Create a DTO - your DTO will define an ID property which will need to be specified as the
TKey
generic parameter. - Create a data proxy that implements
IDataProxy<T, TKey>
or by creating a data proxy that inherits from one of these classes. - 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 : Peasy.Core.ServiceBase<Customer, int>
{
public CustomerService(Peasy.Core.IDataProxy<Customer, int> dataProxy) : base(dataProxy)
{
}
}
The CustomerService class inherits from Peasy.Core.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.
Peasy.Core.ServiceBase and Peasy.BusinessServiceBase expose commands for invoking create, retrieve, update, and delete (CRUD) operations against the injected [data proxies] (https://github.com/ahanusa/Peasy.NET/wiki/Data-Proxy). 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/ahanusa/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.
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)
};
}
}
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)
};
}
}
Initialization logic can be helpful when you need to initialize a DTO with required values before it is subjected to validations, or when you want to provide logging at every step of a command's execution.
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.
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.
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:
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.
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>
.
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/ahanusa/Peasy.NET/blob/master/Peasy.Core/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.
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).GetBusinessRulesResults();
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.