繁体   English   中英

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

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

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

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

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

System.MissingMethodException: '未找到类型 'AspNetCoreRulesChainSample.Rules.ShoppingCartRules.IsValidCupomRule' 的构造函数。

问题是什么?

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

这个怎么运作?

基本上所有规则都有一个ShouldRun方法,该方法定义是否应将 run 方法称为应用业务规则的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()方法上这样做。

这methos只是反转链上的每一个规则的顺序,并创建每个规则的新实例,并将最后一个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;
    }
}

这是我如何使用下一个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>类和IServiceCollection依赖项实例化一个新类?


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

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

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

如果目的是允许通过构造函数注入进行依赖注入,则需要重构类的当前设计以实现这一点。

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

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

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

有效杯规则

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

一阶折扣规则

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM