I'v developed a Rules Engine library, called RulesChain, that works perfectly when the rules doesn't need that any dependency to be injected.
The primary goal with this library is to simplify writing business rules in .NET environment based on Rules Design Pattern and Chain of Responsability Pattern to work like ASP.Net Core middlewares works.
When I need to have any dependency injected I get this error:
System.MissingMethodException: 'Constructor on type 'AspNetCoreRulesChainSample.Rules.ShoppingCartRules.IsValidCupomRule' not found.'
What is the problem?
My abstract Rule class needs to receive the next rule to be called on it's constructor. But I can't add the put an specific Rule on constructor because the chain is resolved on RuleChain
class
How it Works?
Basically all rules have a ShouldRun
method that defines if the run method should be called a Run
method that applies the Business Rule. And the Invoke
method that is called inside the rule when it needs to call the next rule.
This is the rule with dependency injection that throws an error:
public class IsValidCupomRule : Rule<ApplyDiscountContext>
{
private ISalesRepository _salesRepository;
public IsValidCupomRule(Rule<ApplyDiscountContext> next, ISalesRepository salesRepository) : base(next)
{
_salesRepository = salesRepository;
}
public override ApplyDiscountContext Run(ApplyDiscountContext context)
{
// Gets 7% of discount;
var myDiscount = context.Context.Items.Sum(i => i.Price * 0.07M);
context = _next.Invoke(context) ?? context;
// Only apply first order disccount if the discount applied by the other rules are smaller than this
if (myDiscount > context.DiscountApplied)
{
context.DiscountApplied = myDiscount;
context.DiscountTypeApplied = "Cupom";
}
return context;
}
public override bool ShouldRun(ApplyDiscountContext context)
{
return !string.IsNullOrWhiteSpace(context.Context.CupomCode)
&& context.Context.Items?.Count > 1
&& _salesRepository.IsCupomAvaliable(context.Context.CupomCode);
}
}
A basic rule without dependency is like that.
public class BirthdayDiscountRule : Rule<ApplyDiscountContext>
{
public BirthdayDiscountRule(Rule<ApplyDiscountContext> next) : base(next)
{ }
public override ApplyDiscountContext Run(ApplyDiscountContext context)
{
// Gets 10% of discount;
var birthDayDiscount = context.Context.Items.Sum(i => i.Price * 0.1M);
context = _next.Invoke(context);
// Only apply birthday disccount if the discount applied by the other rules are smaller than this
if (birthDayDiscount > context.DiscountApplied)
{
context.DiscountApplied = birthDayDiscount;
context.DiscountTypeApplied = "Birthday Discount";
}
return context;
}
public override bool ShouldRun(ApplyDiscountContext context)
{
var dayAndMonth = context.ClientBirthday.ToString("ddMM");
var todayDayAndMonth = DateTime.Now.ToString("ddMM");
return dayAndMonth == todayDayAndMonth;
}
}
And the abstract rule is that:
public abstract class Rule<T> : IRule<T>
{
protected readonly Rule<T> _next;
protected Rule(Rule<T> next)
{
_next = next;
}
/// <summary>
/// Valides if the rules should be executed or not
/// </summary>
/// <returns></returns>
public abstract bool ShouldRun(T context);
/// <summary>
/// Executes the rule
/// </summary>
/// <returns></returns>
public abstract T Run(T context);
public virtual T Invoke(T context)
{
if(ShouldRun(context))
return Run(context);
else
return _next != null
? _next.Invoke(context)
: context;
}
}
To create my chain of rules I just need to do this:
public ShoppingCart ApplyDiscountOnShoppingCart(ShoppingCart shoppingCart)
{
// Create the chain
var shoppingCartRuleChain = new RuleChain<ApplyDiscountContext>()
.Use<IsValidCupomRule>()
.Use<BirthdayDiscountRule>()
.Use<FirstOrderDiscountRule>()
.Build();
// Create the chain context
var shoppingCartRuleContext = new ApplyDiscountContext(shoppingCart);
shoppingCartRuleContext.Properties["IsFirstOrder"] = true;
shoppingCartRuleContext.ClientBirthday = DateTime.UtcNow;
// Invoke the RulesChain
shoppingCartRuleContext = shoppingCartRuleChain.Invoke(shoppingCartRuleContext);
// Get data form the Chain result and return a ShoppingCart with new data.
shoppingCart.Discount = shoppingCartRuleContext.DiscountApplied;
shoppingCart.DiscountType = shoppingCartRuleContext.DiscountTypeApplied;
return shoppingCart;
}
The principal point for me here is that I can put any Rule in the .Use<IRule>()
call and it permits that the rules
does not depends on each other and the chain can be changed without the need of refactoring each rule. I'm doing this on Build()
method.
This methos just invert the order of each rule on the chain and creates a new instance of each rule, and adds the last Rule
instance as the next Rule
of he new Rule
.
This is the RuleChain class
public class RuleChain<T> : IRuleChain<T>
{
private readonly IList<Type> _components = new List<Type>();
public IRuleChain<T> Use<TRule>()
{
_components.Add(typeof(TRule));
return this;
}
public IRule<T> Build()
{
IRule<T> app = EndOfChainRule<T>.EndOfChain();
foreach (var component in _components.Reverse())
{
app = (IRule<T>)Activator.CreateInstance(component,app);
}
return app;
}
}
Here is how I instantiate new Rules
with the next Rule
: app = (IRule<T>)Activator.CreateInstance(component,app);
Other information that may be useful:
This is how I resolve Dependencies on IoC module
public static class Modules
{
public static void AddRepository(this IServiceCollection services)
{
services.AddScoped<ISalesRepository, SalesRepository>();
}
public static void AddRules(this IServiceCollection services)
{
services.AddScoped<IsValidCupomRule>();
services.AddScoped<FirstOrderDiscountRule>();
services.AddScoped<BirthdayDiscountRule>();
services.AddScoped<ShoppingCartRulesChain>();
}
}
My startup.cs Configure is this:
public void ConfigureServices(IServiceCollection services)
{
services.AddRepository();
services.AddRules();
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
Whats my question?
How can I instantiate a new class based on the same Rule<T>
class and with dependencies of IServiceCollection
?
The RulesChain source code is available at: https://github.com/lutticoelho/RulesChain
This sample source code is available at: https://github.com/lutticoelho/AspNetCoreRulesChainSample
If anyone needs more information about, or to put more code on the question feel free to ask at the comments and I'll provide any changes needed in this question.
Now there is a lot to unpack here. First observation would be with the RuleChain
class.
If the intention is to allow for Dependency Injection via constructor injection then the current design of the class will need to be refactored to allow for that.
Since the current design is modeled behind the Asp.Net Core Middleware pipeline, I would suggest using delegates to store and handle the desired invocation.
First define a delegate
to handle the rule processing
/// <summary>
/// A function that can process a <see cref="TContext"/> dependent rule.
/// </summary>
/// <typeparam name="TContext"></typeparam>
/// <param name="context"></param>
/// <returns>A task that represents the completion of rule processing</returns>
public delegate Task RuleHandlingDelegate<TContext>(TContext context);
The advantage of using the delegate is that it can be late bound to an actual implementation after all the necessary dependencies have be resolved.
Also note that this generic delegate definition uses Task
to allow for asynchronous operations
This does require a change to the IRuleChain<T>
definition.
/// <summary>
/// Defines a class that provides the mechanisms to configure an application's rules pipeline execution.
/// </summary>
/// <typeparam name="TContext">The context shared by all rules in the chain</typeparam>
public interface IRuleChain<TContext> {
/// <summary>
/// Adds a rule to the application's request chain.
/// </summary>
/// <returns>The <see cref="IRuleChain{T}"/>.</returns>
IRuleChain<TContext> Use<TRule>();
/// <summary>
/// Builds the delegate used by this application to process rules executions.
/// </summary>
/// <returns>The rules handling delegate.</returns>
RuleHandlingDelegate<TContext> Build();
}
And the implementation.
In order to allow other arguments to be injected into the rule implementation, the chain will need to be able to resolve constructor argument.
public abstract class RuleChain<TContext> : IRuleChain<TContext> {
private readonly Stack<Func<RuleHandlingDelegate<TContext>, RuleHandlingDelegate<TContext>>> components =
new Stack<Func<RuleHandlingDelegate<TContext>, RuleHandlingDelegate<TContext>>>();
private bool built = false;
public RuleHandlingDelegate<TContext> Build() {
if (built) throw new InvalidOperationException("Chain can only be built once");
var next = new RuleHandlingDelegate<TContext>(context => Task.CompletedTask);
while (components.Any()) {
var component = components.Pop();
next = component(next);
}
built = true;
return next;
}
public IRuleChain<TContext> Use<TRule>() {
components.Push(createDelegate<TRule>);
return this;
}
protected abstract object GetService(Type type, params object[] args);
private RuleHandlingDelegate<TContext> createDelegate<TRule>(RuleHandlingDelegate<TContext> next) {
var ruleType = typeof(TRule);
MethodInfo methodInfo = getValidInvokeMethodInfo(ruleType);
//Constructor parameters
object[] constructorArguments = new object[] { next };
object[] dependencies = getDependencies(ruleType, GetService);
if (dependencies.Any())
constructorArguments = constructorArguments.Concat(dependencies).ToArray();
//Create the rule instance using the constructor arguments (including dependencies)
object rule = GetService(ruleType, constructorArguments);
//return the delegate for the rule
return (RuleHandlingDelegate<TContext>)methodInfo
.CreateDelegate(typeof(RuleHandlingDelegate<TContext>), rule);
}
private MethodInfo getValidInvokeMethodInfo(Type type) {
//Must have public method named Invoke or InvokeAsync.
var methodInfo = type.GetMethod("Invoke") ?? type.GetMethod("InvokeAsync");
if (methodInfo == null)
throw new InvalidOperationException("Missing invoke method");
//This method must: Return a Task.
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
throw new InvalidOperationException("invalid invoke return type");
//and accept a first parameter of type TContext.
ParameterInfo[] parameters = methodInfo.GetParameters();
if (parameters.Length != 1 || parameters[0].ParameterType != typeof(TContext))
throw new InvalidOperationException("invalid invoke parameter type");
return methodInfo;
}
private object[] getDependencies(Type middlewareType, Func<Type, object[], object> factory) {
var constructors = middlewareType.GetConstructors().Where(c => c.IsPublic).ToArray();
var constructor = constructors.Length == 1 ? constructors[0]
: constructors.OrderByDescending(c => c.GetParameters().Length).FirstOrDefault();
if (constructor != null) {
var ctorArgsTypes = constructor.GetParameters().Select(p => p.ParameterType).ToArray();
return ctorArgsTypes
.Skip(1) //Skipping first argument since it is suppose to be next delegate
.Select(parameter => factory(parameter, null)) //resolve other parameters
.ToArray();
}
return Array.Empty<object>();
}
}
With this abstract chain, it is now the responsibility of it's implementation to define how to resolve any dependencies.
Following the sample context, that is simple enough. Since using the default DI extension, then the chain should depend on IServiceProvider
for types whose arguments are not known and Activator
for those with provided constructor arguments.
public class DiscountRuleChain : RuleChain<ApplyDiscountContext> {
private readonly IServiceProvider services;
public DiscountRuleChain(IServiceProvider services) {
this.services = services;
}
protected override object GetService(Type type, params object[] args) =>
args == null || args.Length == 0
? services.GetService(type)
: Activator.CreateInstance(type, args);
}
With all of the above provided so far, there were some changes that allowed for a cleaner design.
Specifically IRule<TContext>
and its default implementation.
public interface IRule<TContext> {
Task Invoke(TContext context);
}
public abstract class Rule<TContext> : IRule<TContext> {
protected readonly RuleHandlingDelegate<TContext> next;
protected Rule(RuleHandlingDelegate<TContext> next) {
this.next = next;
}
public abstract Task Invoke(TContext context);
}
Any Context specific rules can now be abstracted to target a specific domain
For example
public abstract class DiscountRule : Rule<ApplyDiscountContext> {
protected DiscountRule(RuleHandlingDelegate<ApplyDiscountContext> next) : base(next) {
}
}
This simplified the implementations specific to discounts in the sample and allowed for dependencies to be injected
IsValidCupomRule
public class IsValidCupomRule : DiscountRule {
private readonly ISalesRepository _salesRepository;
public IsValidCupomRule(RuleHandlingDelegate<ApplyDiscountContext> next, ISalesRepository salesRepository)
: base(next) {
_salesRepository = salesRepository;
}
public override async Task Invoke(ApplyDiscountContext context) {
if (cupomAvailable(context)) {
// Gets 7% of discount;
var myDiscount = context.Context.Items.Sum(i => i.Price * 0.07M);
await next.Invoke(context);
// Only apply discount if the discount applied by the other rules are smaller than this
if (myDiscount > context.DiscountApplied) {
context.DiscountApplied = myDiscount;
context.DiscountTypeApplied = "Cupom";
}
} else
await next(context);
}
private bool cupomAvailable(ApplyDiscountContext context) {
return !string.IsNullOrWhiteSpace(context.Context.CupomCode)
&& context.Context.Items?.Count > 1
&& _salesRepository.IsCupomAvaliable(context.Context.CupomCode);
}
}
FirstOrderDiscountRule
public class FirstOrderDiscountRule : DiscountRule {
public FirstOrderDiscountRule(RuleHandlingDelegate<ApplyDiscountContext> next) : base(next) { }
public override async Task Invoke(ApplyDiscountContext context) {
if (shouldRun(context)) {
// Gets 5% of discount;
var myDiscount = context.Context.Items.Sum(i => i.Price * 0.05M);
await next.Invoke(context);
// Only apply discount if the discount applied by the other rules are smaller than this
if (myDiscount > context.DiscountApplied) {
context.DiscountApplied = myDiscount;
context.DiscountTypeApplied = "First Order Discount";
}
} else
await next.Invoke(context);
}
bool shouldRun(ApplyDiscountContext context) {
return (bool)(context.Properties["IsFirstOrder"] ?? false);
}
}
The following test was used to verify expected behavior of the rules engine.
[TestClass]
public class RulesEngineTests {
[TestMethod]
public async Task Should_Apply_Cupom_Discount() {
//Arrange
var cupomCode = "cupomCode";
var services = new ServiceCollection()
.AddSingleton<ISalesRepository>(sp =>
Mock.Of<ISalesRepository>(_ => _.IsCupomAvaliable(cupomCode) == true)
)
.BuildServiceProvider();
// Create the chain
var shoppingCartRuleChain = new DiscountRuleChain(services)
.Use<IsValidCupomRule>()
.Use<FirstOrderDiscountRule>()
.Build();
ShoppingCart shoppingCart = new ShoppingCart {
CupomCode = cupomCode,
Items = new List<ShoppingCartItem> {
new ShoppingCartItem { Price = 10M },
new ShoppingCartItem { Price = 10M },
}
};
var expectedDiscountType = "Cupom";
var expectedDiscountApplied = 1.40M;
// Create the chain context
var shoppingCartRuleContext = new ApplyDiscountContext(shoppingCart);
shoppingCartRuleContext.Properties["IsFirstOrder"] = true;
shoppingCartRuleContext.ClientBirthday = DateTime.UtcNow;
//Act
await shoppingCartRuleChain.Invoke(shoppingCartRuleContext);
// Get data from the context result and verify new data.
shoppingCart.Discount = shoppingCartRuleContext.DiscountApplied;
shoppingCart.DiscountType = shoppingCartRuleContext.DiscountTypeApplied;
//Assert (using FluentAssertions)
shoppingCart.DiscountType.Should().Be(expectedDiscountType);
shoppingCart.Discount.Should().Be(expectedDiscountApplied);
}
}
Note how the dependency to be injected was mocked to test the expected behavior in isolation.