-
Notifications
You must be signed in to change notification settings - Fork 69
Business Rule Configuration
Rules within the Peasy framework have been written to allow you to configure rules with maximum flexibility using an expressive syntax.
- Configuring rules in ServiceBase
- Configuring rules in a Command
- Chaining business rules
- Executing code on failed validation of a business rule
- Executing code on successful validation of a business rule
- Testing rule configurations
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.
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.
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)
};
}
}
Business rule execution can be expensive, especially if a rule requires data from a data source which could result in a hit to a database or a call to a an external HTTP service. To help circumvent potentially expensive data retrievals, RuleBase
exposes IfValidThenValidate
, which accepts a list of IRule
, and will only be validated in the event that the parent rule's validation is successful.
Let's take a look at an example:
protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
return TheseRules
(
new SomeRule().IfValidThenValidate(new ExpensiveRule(_someDataProxy))
);
}
In this example, we configure the parent rule SomeRule
and specify that upon successful validation, it should validate ExpensiveRule
, who requires a data proxy and will most likely perform a method invocation to retrieve data for validation. It's important to note that the error message of a parent rule will be set to it's child rule should it's child fail validation.
Let's look at another example and introduce another rule that's really expensive to validate, as it requires getting data from two data proxies.
protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
return TheseRules
(
new SomeRule().IfValidThenValidate
(
new ExpensiveRule(_someDataProxy),
new TerriblyExpensiveRule(_anotherDataProxy, _yetAnotherDataProxy)
)
);
}
In this example, both ExpensiveRule and TerriblyExpensiveRule will only be validated upon successful validation of SomeRule. But what if we only wanted each rule to be validated upon successful validation of its predecessor?
Here's how that might look:
protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
return TheseRules
(
new SomeRule().IfValidThenValidate(
new ExpensiveRule(_someDataProxy).IfValidThenValidate(
new TerriblyExpensiveRule(_anotherDataProxy, _yetAnotherDataProxy)));
)
}
Next let's look at validating a set of rules based on the successful validation of another set of rules.
protected override async Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
var baseRules = await base.OnInsertCommandGetRulesAsync(resource, context);
baseRules.IfAllValidThenValidate
(
new ExpensiveRule(_someDataProxy),
new TerriblyExpensiveRule(_anotherDataProxy, _yetAnotherDataProxy)
);
return baseRules;
}
In this scenario, we have overridden OnInsertCommandGetRulesAsync
and want to ensure that all of the rules defined in the base implementation are executed successfully before validating our newly defined rules.
Sometimes you might want to execute some logic based on the failed validation of a business rule.
Here's how that might look:
protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
return TheseRules
(
new SomeRule().IfInvalidThenInvoke(async (rule) => await _logger.LogErrorAsync(rule.ErrorMessage))
);
}
Sometimes you might want to execute some logic based on the successful validation of a business rule.
Here's how that might look:
protected override Task<IEnumerable<IRule>> OnInsertCommandGetRulesAsync(Customer resource, ExecutionContext<Customer> context)
{
return TheseRules
(
new SomeRule().IfValidThenInvoke(async (rule) => await _logger.LogSuccessAsync("Your success details))
);
}