c# - 具有编程依赖注入(inject)的 Asp.Net Core 规则引擎 - 未找到类型 'type' 的构造函数

标签 c# asp.net-core dependency-injection architecture rule-engine

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

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

当我需要注入(inject)任何依赖项时,我收到此错误:

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

问题是什么?

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

它是如何工作的?

基本上所有规则都有 ShouldRun定义是否应调用 run 方法的方法 Run应用业务规则的方法。还有Invoke当需要调用下一个规则时,规则内部调用的方法。

这是抛出错误的依赖注入(inject)规则:

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下一个实例 Rule新的Rule .

这是 RuleChain 类

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

这是我实例化新的 Rules 的方法与下一个Rule :app = (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> 实例化一个新类类和依赖项 IServiceCollection


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

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

最佳答案

现在这里有很多东西需要解压。第一个观察是 RuleChain类。

如果目的是允许通过构造函数注入(inject)进行依赖注入(inject),则需要重构该类的当前设计以允许这样做。

由于当前的设计是在 Asp.Net Core 中间件管道后面建模的,因此我建议使用委托(delegate)来存储和处理所需的调用。

首先定义一个 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);

使用委托(delegate)的优点是,在解决所有必要的依赖关系后,它可以后期绑定(bind)到实际实现。

另请注意,此通用委托(delegate)定义使用 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();
}

以及实现。

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

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

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

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

请注意如何模拟要注入(inject)的依赖项以隔离测试预期行为。

关于c# - 具有编程依赖注入(inject)的 Asp.Net Core 规则引擎 - 未找到类型 'type' 的构造函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59513882/

相关文章:

c# - 包管理器控制台 Visual Studio 2019 返回新行

c# - 我如何使用简单的注入(inject)器将一个类型及其接口(interface)注册为单例?

asp.net - 基于microsoft/aspnet的Docker容器无法加载Kestrel

c# - 优雅的方式解析URL

c# - VS2010记事本打开cs文件

c# - 将类方法转换为IL并在运行时执行

asp.net-core - 检查 RenderFragment 是否为空

java - 注入(inject)命名的 Guice 单例

java - 如何将代理注入(inject)服务?

c# - 为代理提供用户名和密码 - Selenium