Search code examples
c#.netdependency-injection.net-6.0object-lifetime

Services in new scope act like transient services


I have a method where I need a new scope (must be a new scope, the scenario here doesn't need it, but when this works I'll use the logic elsewhere where it needs to be a separate scope), for this I use the IServiceScopeFactory (I think that's the right one). I then get the services I need out of the new scope and I expect them to still work in a scoped way. But dependencies in those services act like transient services. I always get a new one in the constructor.

Example:


public class EntryController : IEntryController
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly IRequestContext _requestContext;
    
    public EntryController(IServiceScopeFactory scopeFactory, IRequestContext requestContext)
    {
        _scopeFactory = scopeFactory;
        _requestContext = requestContext;
    }

    public async Task GetEntries(int userId)
    {
        using var scope = _scopeFactory.CreateScope();
        var requestContext = scope.ServiceProvider.GetRequiredService<IRequestContext>();
        var manager = scope.ServiceProvider.GetRequiredService<IEntryManager>();

        var test = requestContext // Completely new IRequestContext
        requestContext = _requestContext;
        var test1 = requestContext // test1 is the same as _requestContext, which is good

        return await manager.GetAll();
    }
}
public class EntryManager : IEntryManager
{
    private readonly IEntryResource _entryResource;
    private readonly IRequestContext _requestContext;

    public EntryManager (IEntryResource entryResource, IRequestContext requestContext)
    {
        _entryResource = entryResource;
        _requestContext = requestContext;
    }

    public async Task GetAll()
    {
        var test = _requestContext; // Completely new IRequestContext, which is bad
        return await _entryResource.GetAll();
    }
}
public class EntryResource : IEntryResource
{
    private readonly IRequestContext _requestContext;

    public EntryManager (IRequestContext requestContext)
    {
        _requestContext = requestContext;
    }

    public async Task GetAll()
    {
        var test = _requestContext; // Completely new IRequestContext, which is bad
        // here is some code for the db query where I need info stored in the IRequestContext
        return _dbContext.Entries.ToListAsync();
    }
}

I understand why I get a new requestContext in the new scope, but when I update the values I would expect those to be available inside the whole scope through dependency injection. When I run the code without a new scope everything works fine. All services are added as scoped services in the startup.

services.AddScoped<IRequestContext, RequestContext>();
services.AddScoped<IEntryManager,EntryManager>();
services.AddScoped<IEntryResource, EntryResource>();

Solution

  • I found the issue:

    requestContext = _requestContext;
    

    This didn't work because requestContext is no longer the same object as it was before this line and it will no longer be the same as the one injected to the other services within the scope.

    If I simply assign new values to its properties it does work. this is why this works:

    requestContext.Id = _requestContext.Id
    

    That way it still is the same object and the new value of Id can be retrieved everywhere in the new scope inside the requestContext.

    With a custom extension I can assign all properties.

    public static IRequestContext CopyRC(this IRequestContext dest, IRequestContext source)
    {
        var props = typeof(RequestContext).GetProperties();
                
        foreach(var prop in props)
        {
            var value = prop.GetValue(source);
            prop.SetValue(dest, value);
        }
        return dest;
    }
    

    With this

    requestContext = _requestContext;
    

    now becomes

    requestContext.CopyRC(_requestContext);
    

    Thanks to Stephen Cleary for helping me look in the right direction with his working snippet.