NRules with Asp.Net Core scoped dependency could not be resolved

432 views Asked by At

I am getting the following error when I run the sample here.

Cannot resolve scoped service 'IPersonService' from root provider.

I want to inject the service IPersonService into the rule MalePersonRule

Note that this same service IPersonService is injected into the HomeController.

I have registered this IPersonService as scoped service in the Startup.cs class.

I used this NRules.Integration.AspNetCore nuget package and got the error. So to understand more I included the source of that nuget from this git hub repo into my solution in this folder.

How do I register a service in request scope so that that service will be injected into the Rule as well with the same scope? Is this possible?

I understood that the Rule cannot be at Request scope. The Rule is instantiated at the start of the application and then the same is used for the rest of the life time for all the requests. I want the service dependencies to be at request scope, meaning that service to be instantiated afresh for each new request. Is this possible?

But I guess the rule session is created afresh for each request.

So far I was not able to integrate NRules into AspNetCore project satisfactorily. I studied this NRules.Demo project also to get an idea of how to integrate, but facing similar problems. If there is any aspnet core demo project showing how to integrate please do share.

What am I missing?

The working solution to reproduce the problem is here for your reference.

 NRules.RuleRhsExpressionEvaluationException
   HResult=0x80131500
   Message=Failed to evaluate rule action
 (ctx, serviceProvider) => WriteLine(Convert(Convert(serviceProvider.GetService(NRulesWithAspNetCore.Services.IPersonService), IPersonService).Id, Object))
 NRulesWithAspNetCore.Rules.MalePersonRule
   Source=NRules
   StackTrace:
    at NRules.ActionExecutor.Execute(IExecutionContext executionContext, IActionContext actionContext)
    at NRules.Session.Fire(Int32 maxRulesNumber, CancellationToken cancellationToken)
    at NRules.Session.Fire(CancellationToken cancellationToken)
    at NRules.Session.Fire()
    at NRulesWithAspNetCore.Controllers.HomeController.Index() in D:\Trials\WorkFlow\ElsaBugReports\src\NR1003002NRulesAspNetCore\NR1003002NRulesAspNetCore\Controllers\HomeController.cs:line 46
    at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
    at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<<InvokeActionMethodAsync>g__Logged|12_1>d.MoveNext()
 
   This exception was originally thrown at this call stack:
     [External Code]
 
 Inner Exception 1:
 InvalidOperationException: Cannot resolve scoped service 'NRulesWithAspNetCore.Services.IPersonService' from root provider.
 

UPDATE:

Hi Sergiy,

Thank you. Your solution is working. But I found another way.

This is using a middleware. I got the clue reading this SO answer.

So I created a middleware as follows.

public class NRulesMiddleWare
{
    private readonly RequestDelegate _next;

    public NRulesMiddleWare(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, 
        ISessionFactory sessionFactory, IDependencyResolver dependencyResolver)
    {
        // Reference: https://stackoverflow.com/a/48591356/1977871

        // Note 1. sessionFactory object is singleton meaning, its created at the start of the application.
        // Then its the same instance is used throughout the applicatioin life cycle.
        // But then the dependency resolver needs to be assigned everytime a request arrives.
        // If dependencyResolver is assigned rigth in the beginning at the time of registration of 
        // sessionFactory, then its not working. I am Not sure why. But using this middleware, its working.

        // Note 2. Here the dependency resolver instance is assigned afresh for each request.

        sessionFactory.DependencyResolver = dependencyResolver;
        await _next.Invoke(context);
    }
}

Note that the invoke method above has a dependency on IDependencyResolver, so that needs to be registered. And ofcourse the middleware itself also has to be registered.

So in summary, 3 changes

  1. Include the above middle ware in the solution.
  2. Register it Startup class Configure method. app.UseMiddleware();
  3. And finally, register IDependencyResolver with the service collection in ConfigureServices. services.AddScoped<IDependencyResolver, AspNetCoreDependencyResolver>();

Your approach uses the new keyword to instantiate the resolver. In the DI world, I read somewhere, this newing up is a cardinal sin. Object creation must be the responsibility of DI container(ServiceCollection) object. So I feel middleware approach is better.

1

There are 1 answers

0
Sergiy Nikolayev On

In the demo project that you provided, IDependencyResolver is hooked up to NRules ISessionFactory with the root IServiceProvider. Every session inherits that root resolver, hence the error, as a scoped service cannot be resolved via the root container.

You can keep the default root resolver at the factory level. But make the following change to the session registration code, to set up a session-specific IDependencyResolver with a scoped IServiceProvider. This fixes the issue as the dependency is now resolved via the scoped provider associated with the session.

Before:

private static IServiceCollection RegisterSession(this IServiceCollection services)
{
    return services.AddScoped<ISession>(c => services.BuildServiceProvider().GetRequiredService<ISessionFactory>().CreateSession());
}

After:

private static IServiceCollection RegisterSession(this IServiceCollection services)
{
    return services.AddScoped<ISession>(c =>
    {
        var factory = services.BuildServiceProvider().GetRequiredService<ISessionFactory>();
        var session = factory.CreateSession();
        session.DependencyResolver = new AspNetCoreDependencyResolver(c);
        return session;
    });
}

Since this code lives in https://github.com/cloudb0x/NRules.Integration.AspNetCore project, you can work with the author of that package to contribute the change, or you can implement your own session-level IDependencyResolver in the way I described.