Skip to content

IServiceScopeFactory is Documented Assumed to be a Singleton #67391

@tillig

Description

@tillig

Description

In the performance documentation for ASP.NET Core there is an example showing the use of IServiceScopeFactory from request services but making use of that factory after the request services set has been disposed.

[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

This documentation implies that the IServiceScopeFactory that is provided by request services is a singleton, or at least that it always provides service scopes that come from the root application container rather than being hierarchical. If this wasn't the case, then the parent scope that the IServiceScopeFactory came from would already have been disposed and you wouldn't be able to create a nested scope.

Basically, the documentation shows this:

  • Root IServiceProvider
    • Request services scope
    • Scope created by IServiceScopeFactory

When, if scopes are hierarchical, it'd be more like:

  • Root IServiceProvider
    • Request services scope
      • Scope created by IServiceScopeFactory (which couldn't happen, because request services has already been disposed)

There is no DI specification test indicating this all-scopes-must-come-from-the-root behavior, so either there's a documentation problem or there's a breaking change that will end up having to get introduced into the specification tests to make sure every conforming container implementation adheres to this two-level-only scope mechanism.

While I recognize that the context here is ASP.NET, since the DI bits are in this repo it seemed reasonable to start here. The behavior is more about what's expected from the DI implementation than it is about how ASP.NET consumes it.

Possible overlap with dotnet/aspnetcore#31478 where there is some discussion about the assumption IServiceScopeFactory is a singleton, but again, without any spec tests to enforce that and without discussing that decision with the community. This is only how the Microsoft container works, not how every conforming container works.

Reproduction Steps

Implement an ASP.NET app (or, I guess, a Console app) that performs effectively this sort of multi-threaded handling of scopes:

[HttpGet("/fire-and-forget-3")]
public IActionResult FireAndForget3([FromServices]IServiceScopeFactory 
                                    serviceScopeFactory)
{
    _ = Task.Run(async () =>
    {
        await Task.Delay(1000);

        using (var scope = serviceScopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<ContosoDbContext>();

            context.Contoso.Add(new Contoso());

            await context.SaveChangesAsync();                                        
        }
    });

    return Accepted();
}

Under the Microsoft container, this passes. Now back it with Autofac, which enforces hierarchical scopes. You'll fail because the request services scope has been disposed and you can't create a new child scope from a scope that's disposed.

Expected behavior

I expect the documentation to show an example that works with the default container and with conforming containers that adhere to the specification tests.

Actual behavior

Microsoft DI works, other conforming containers may or may not work despite passing the spec tests.

Regression?

Not a regression that I'm aware of.

Known Workarounds

Folks using other containers will have to use backing-container-specific workarounds. For example, instead of injecting an IServiceScopeFactory, folks wanting to do this with Autofac will need to hold a global static reference to the root container and manually create a child scope from that root container.

Configuration

The configuration, in this case, doesn't matter, though I'm on .NET 6 and the documentation is for .NET 6 that I linked.

Other information

The documentation should show something that works with both Microsoft and other conforming container providers. If that means that documentation needs to be removed because it's wrong, remove it. If it means introducing new requirements for conforming containers, that's definitely something to get the rest of the community involved in because it's going to break some folks who don't make this assumption.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions