12

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

}
Steven
  • 166,672
  • 24
  • 332
  • 435
Joseph Gabriel
  • 8,339
  • 3
  • 39
  • 53
  • Are you sure the controller code is executed before the middleware code? – Stilgar Feb 23 '18 at 13:51
  • Please post a Minimal, Complete, Verifiable example. For instance, show the registrations for `FooService` and `Foo`. – Steven Feb 23 '18 at 13:53
  • Yes, I have stepped through it many times, with different scenarios. Plus, I don't believe the response would start before the controller action ended. – Joseph Gabriel Feb 23 '18 at 13:54
  • 1
    @Stilgar I would imagine that's the case as `Response.OnStarting` happens when the response headers are sent, long after the request has started. – DavidG Feb 23 '18 at 13:54
  • But I bet that you've registered `FooService` as `Transient` instead of `Scoped`. – Steven Feb 23 '18 at 13:55
  • @Steven I changed it to Singleton, and it works, back to Scoped, and it does not. I haven't even tried transient for this. – Joseph Gabriel Feb 23 '18 at 13:56
  • Try injecting `FooService` into `AppMessageMiddleware`'s constructor instead. – Steven Feb 23 '18 at 13:58
  • I'm wondering if it has something to do with the middleware - or perhaps I'm not injecting it properly in the middleware, since that's were I'm explicitly injecting it. – Joseph Gabriel Feb 23 '18 at 13:58
  • 1
    You shouldn't be using `GetService` anyway, I wonder if that is using the `ApplicationServices` container which is effectively the same as making it a singleton. Instead you should inject the service in the `Invoke` method as a parameter. – DavidG Feb 23 '18 at 14:10
  • That's not how DI is supposed to work. Middleware services are no different than controllers or any other class - they should *not* request instances from the IServiceProvider and hence don't need to call it at all. `IServiceProvider` is the one that should inject whatever they need in their constructors, when it creates them. If your class needs a `FooService`, add a `FooService` parameter to the constructor – Panagiotis Kanavos Feb 23 '18 at 14:26

2 Answers2

14

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);    
}
Evk
  • 98,527
  • 8
  • 141
  • 191
6

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

    }
}
DavidG
  • 113,891
  • 12
  • 217
  • 223
  • Thank you. Your second reason - that was my hunch, hence my attempt at retrieving the service instance inside the OnStarting() method using `serviceProvider.GetService()` – Joseph Gabriel Feb 23 '18 at 15:01
  • I'll read the discussion you referenced - not sure yet if there is a good solution. I need to access the scoped service and modify the response accordingly - but I'd like to do it in a "proper" manner rather than trying to use HttpContext directly from `FooService` – Joseph Gabriel Feb 23 '18 at 15:03
  • I'm confused, there's nothing here to suggest you need the HTTP context in `FooService`? – DavidG Feb 23 '18 at 15:04
  • As part of executing a controller action, I am adding instances of Foo to the List exposed by the scoped FooService. This may happen anywere (directly in the controller, repository, etc.) Then, before the response is sent, I am setting the response header according to the information collected by the FooService. – Joseph Gabriel Feb 23 '18 at 15:08
  • Yes, but why does that mean `FooService` needs to know anything about the context? – DavidG Feb 23 '18 at 15:09
  • It wasn't my first choice, but I am considering it as a possible alternative, because looks like I can't access the Scoped service from within the Middleware. The middleware's only purpose was to modify the response according to the information in FooService. Can you recommend any other solutions? – Joseph Gabriel Feb 23 '18 at 15:11
  • never mind - between the two good answers here, you might be able to knock some sense into me. I think I understand now. I *can* inject into the `Invoke` method, just not the middleware's ctor – Joseph Gabriel Feb 23 '18 at 15:14