Asp.Net 具有编程依赖注入的核心规则引擎 - 未找到类型 'type' 的构造函数

Asp.Net Core Rules Engine with Programmatic Dependency Injection - Constructor on type 'type' not found

我开发了一个名为 RulesChain 的规则引擎库,当规则不需要注入任何依赖项时,它可以完美运行。

此库的主要目标是简化在基于规则设计模式和责任链模式的 .NET 环境中编写业务规则,使其像 ASP.Net 核心中间件一样工作。

当我需要注入任何依赖项时,出现此错误:

System.MissingMethodException: 'Constructor on type 'AspNetCoreRulesChainSample.Rules.ShoppingCartRules.IsValidCupomRule' not found.'

问题是什么?

我的抽象规则 class 需要接收要在其构造函数上调用的下一条规则。但是我无法在构造函数上添加特定规则,因为链是在 RuleChain class

上解析的

它是如何工作的?

基本上所有规则都有一个 ShouldRun 方法来定义 运行 方法是否应该被调用 应用业务规则的 Run 方法。以及需要调用下一条规则时在规则内部调用的 Invoke 方法。

这是引发错误的依赖注入规则:

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

没有依赖的基本规则就是这样。

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;
    }
}

抽象规则是:

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;
    }
}

要创建我的规则链,我只需要这样做:

    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;
    }

我这里的主要观点是我可以在 .Use<IRule>() 调用中放置任何规则,它允许 rules 不相互依赖并且可以在没有需要重构每条规则。我在 Build() 方法上这样做。

这个方法只是颠倒链上每条规则的顺序并为每条规则创建一个新实例,并将最后一个 Rule 实例添加为新 [=26] 的下一个 Rule =].

这是规则链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;
    }
}

下面是我如何用下一个 Rule 实例化新的 Rulesapp = (IRule<T>)Activator.CreateInstance(component,app);

其他可能有用的信息:

这就是我解决 IoC 模块依赖关系的方法

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>();
    }
}

我的startup.cs配置是这样的:

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

我的问题是什么?

如何根据相同的 Rule<T> class 和 IServiceCollection 的依赖项实例化一个新的 class?


RulesChain 源代码位于:https://github.com/lutticoelho/RulesChain
此示例源代码位于:https://github.com/lutticoelho/AspNetCoreRulesChainSample

如果有人需要有关该问题的更多信息或添加更多代码,请随时在评论中提问,我将提供该问题所需的任何更改。

现在这里有很多东西要打开。第一个观察是 RuleChain class.

如果目的是允许通过构造函数注入进行依赖注入,那么 class 的当前设计将需要重构以允许这样做。

由于当前设计是在 Asp.Net 核心中间件管道之后建模的,我建议使用委托来存储和处理所需的调用。

先定义一个delegate来处理规则处理

/// <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);

使用委托的优点是它可以在解决所有必要的依赖关系后延迟绑定到实际实现。

另请注意,此通用委托定义使用 Task 来允许异步操作

这确实需要更改 IRuleChain<T> 定义。

/// <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();
}

以及实施。

为了允许将其他参数注入到规则实现中,链将需要能够解析构造函数参数。

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>();
    }
}

有了这个抽象链,现在它的实现负责定义如何解决任何依赖关系。

按照示例上下文,这很简单。由于使用默认的 DI 扩展,因此对于参数未知的类型,链应依赖于 IServiceProvider,对于提供构造函数参数的类型,链应依赖于 Activator

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

到目前为止提供了以上所有内容,还有一些更改可以实现更简洁的设计。

具体 IRule<TContext> 及其默认实现。

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

现在可以抽象任何上下文特定规则以针对特定域

例如

public abstract class DiscountRule : Rule<ApplyDiscountContext> {
    protected DiscountRule(RuleHandlingDelegate<ApplyDiscountContext> next) : base(next) {
    }
}

这简化了示例中特定于折扣的实现,并允许注入依赖项

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

以下测试用于验证规则引擎的预期行为。

[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);
    }
}

请注意如何模拟要注入的依赖项以单独测试预期行为。