Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Scoped service resolve as two different instances for same request?

I have a simple service that contains a List<Foo>. In Startup.cs, I am using the services.addScoped<Foo, Foo>() method.

I am inject the service instance in two different places (controller and middleware), and for a single request, I would expect to get the same instance. However, this does not appear to be happening.

Even though I am adding a Foo to the List in the Controller Action, the Foo list in the Middleware is always empty. Why is this?

I have tried changing the service registration to a singleton, using AddSingleton() and it works as expected. However, this has to be scoped to the current request. Any help or ideas are greatly appreciated!

FooService.cs

public class FooService
{
    public List<Foo> Foos = new List<Foo>();
}

Startup.cs

...
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddScoped<FooService, FooService>();
}

[Below are the two places where I am injecting the service, resulting in two different instances]

MyController.cs

public class MyController : Controller
{
    public MyController(FooService fooService)
    {
        this.fooService = fooService;
    }

    [HttpPost]
    public void TestAddFoo()
    {
        //add foo to List
        this.fooService.Foos.Add(new Foo());
    }
}

FooMiddleware.cs

public AppMessageMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
{
    this.next = next;
    this.serviceProvider = serviceProvider;
}

public async Task Invoke(HttpContext context)
{
    context.Response.OnStarting(() =>
    {
        var fooService = this.serviceProvider.GetService(typeof(FooService)) as FooService;

        var fooCount = fooService.Foos.Count; // always equals zero

        return Task.CompletedTask;
    });

    await this.next(context);

}
like image 437
Joseph Gabriel Avatar asked Feb 23 '18 13:02

Joseph Gabriel


2 Answers

That's because when you inject IServiceProvider into your middleware - that's "global" provider, not request-scoped. There is no request when your middleware constructor is invoked (middleware is created once at startup), so it cannot be request-scoped container.

When request starts, new DI scope is created, and IServiceProvider related to this scope is used to resolve services, including injection of services into your controllers. So your controller resolves FooService from request scope (because injected to constructor), but your middleware resolves it from "parent" service provider (root scope), so it's different. One way to fix this is to use HttpContext.RequestServices:

public async Task Invoke(HttpContext context)
{
    context.Response.OnStarting(() =>
    {
        var fooService = context.RequestServices.GetService(typeof(FooService)) as FooService;

        var fooCount = fooService.Foos.Count; // always equals zero

        return Task.CompletedTask;
    });

    await this.next(context);    
}

But even better way is to inject it into Invoke method itself, then it will be request scoped:

public async Task Invoke(HttpContext context, FooService fooService)
{
    context.Response.OnStarting(() =>
    {    
        var fooCount = fooService.Foos.Count; // always equals zero

        return Task.CompletedTask;
    });

    await this.next(context);    
}
like image 149
Evk Avatar answered Nov 10 '22 14:11

Evk


First of all you shouldn't be using GetService, use the proper DI system that is in place by passing it into the Invoke method as a parameter.

Secondly, the reason you are getting a different object is because the constructor of the middleware is called outside of the scope of any request, during the app initialisation phase. So the container used there is the global provider. See here for a good discussion.

public class AppMessageMiddleware
{
    private readonly RequestDelegate _next;

    public AppMessageMiddleware(RequestDelegate next, IServiceProvider serviceProvider)
    {
        _next = next;
    }

    //Note the new parameter here:                vvvvvvvvvvvvvvvvvvvvv
    public async Task Invoke(HttpContext context, FooService fooService)
    {
        context.Response.OnStarting(() =>
        {
            var fooCount = fooService.Foos.Count;

            return Task.CompletedTask;
        });

        await _next(context);

    }
}
like image 6
DavidG Avatar answered Nov 10 '22 12:11

DavidG