1
votes

I have a .Net Core service ("MyLookup") that does a database query, some Active Directory lookups, and stores the results to a memory cache.

For my first cut, I did .AddService<>() in Startup.cs, injected the service into the constructors of each of the controllers and views that used the service ... and everything worked.

It worked because my service - and it's dependent services (IMemoryCache and a DBContext) were all scoped. But now I'd like to make this service a singleton. And I'd like to initialize it (perform the DB query, the AD lookups, and save the result to a memory cache) when the app initializes.

Q: How do I do this?

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyDBContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
        services.AddMemoryCache();
        services.AddSingleton<IMyLookup, MyLookup>(); 
        ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
        // Q: Is this a good place to initialize my singleton (and perform the expensive DB/AD lookups?
        app.ApplicationServices.GetService<IDILookup>();   

OneOfMyClients.cs

    public IndexModel(MyDBContext context, IMyLookup myLookup)
    {
        _context = context;
        _myLookup = myLookup;
        ...

MyLookup.cs

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache)
    {
        // Perform some expensive lookups, and save the results to this cache
        _cache = memoryCache;  
    }
    ...
    private async void Rebuild()  // This should only get called once, when app starts
    {
        ClearCache();
        var allNames =  QueryNamesFromDB();
        ...

    private List<string>QueryNamesFromDB()
    {
        // Q: ????How do I get "_context" (which is a scoped dependency)????
        var allNames = _context.MyDBContext.Select(e => e.Name).Distinct().ToList<string>();
        return allSInames;

Some of the exceptions I've gotten trying different things:

InvalidOperationException: Cannot consume scoped service 'MyDBContext' from singleton 'MyLookup'.

... and ...

InvalidOperationException: Cannot resolve scoped service 'MyDBContext' from root provider 'MyLookup'

... or ...

System.InvalidOperationException: Cannot resolve scoped service 'IMyLookup' from root provider.


Thanks to Steve for much valuable insight. I was finally able to:

  1. Create a "lookup" that could be used by any consumer at any time, from any session, during the lifetime of the app.

  2. Initialize it once, at program startup. FYI, it would NOT be acceptable to defer initialization until some poor user triggered it - the initialization simply takes too long.

  3. Use dependent services (IMemoryCache and my DBContext), regardless of those services' lifetimes.

My final code:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDBContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
    services.AddMemoryCache();
    // I got 80% of the way with .AddScoped()...
    // ... but I couldn't invoke it from Startup.Configure().
    services.AddSingleton<IMyLookup, MyLookup>(); 
    ...

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // This finally worked successfully...
    app.ApplicationServices.GetService<IMyLookup>().Rebuild();

OneOfMyClients.cs

public IndexModel(MyDBContext context, IMyLookup myLookup)
{
    // This remained unchanged (for all consumers)
    _context = context;
    _myLookup = myLookup;
    ...

MyLookup.cs

public interface IMyLookup
{
    Task<List<string>> GetNames(string name);
    Task Rebuild();
}

public class MyLookup : IMyLookup
{
    private readonly IMemoryCache _cache;
    private readonly IServiceScopeFactory _scopeFactory;
    ...

    public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
    {
        _cache = memoryCache;
        _scopeFactory = scopeFactory;
    }

    private async void Rebuild()
    {
        ClearCache();
        var allNames =  QueryNamesFromDB();
        ...

    private List<string>QueryNamesFromDB()
    {
        // .CreateScope() -instead of constructor DI - was the key to resolving the problem
        using (var scope = _scopeFactory.CreateScope())
        {
            MyDBContext _context =
                scope.ServiceProvider.GetRequiredService<MyDBContext>();
            var allNames = _context.MyTable.Select(e => e.Name).Distinct().ToList<string>();
            return allNames;
        }
    }
1
You should definitely read about Captive Dependencies. - Steven
And you should read about DI Composition Models. - Steven
I've been doing lots of reading, but I've not seen either of those links. Thank you :) IMPORTANT NOTE: most examples have a "View" or "Controller". I need to initialize my cache before any view or controller gets invoked. I need to use .Net Core DI; I can't use "Ninject" (if that matters). Maybe I can even get my DBContext without any DI at all :) - FoggyDay
@Steven - One of your links pointed to this link, which suggests Instead of injecting the dependency, inject a factory for the creation of that dependency and call that factory every time an instance is required. Q: Sound promising? Q: Any links to how I could do this in .Net Core with my DBContext? - FoggyDay
Why do you need your service to be a singleton in the first place? - Steven

1 Answers

2
votes

There is no one single solution to your problem. At play are different principles, such as the idea of preventing Captive Dependencies, which states that a component should only depend on services with an equal or longer lifetime. This idea pushes towards having a MyLookup class that has either a scoped or transient lifestyle.

This idea comes down to practicing the Closure Composition Model, which means you compose object graphs that capture runtime data in variables of the graph’s components. The opposite composition model is the Ambient Composition Model, which keeps state outside the object graph and allows retrieving state (such as your DbContext) on demand.

But this is all theory. At first, it might be difficult to convert this into practice. (Again) in theory, applying the Closure Composition Model is simple, because it simply means giving MyLookup a shorter lifestyle, e.g. Scoped. But when MyLookup itself captures state that needs to be reused for the duration of the application, this seems impossible.

But this is often not the case. One solution is to extract the state out of the MyLookup, into a dependency that holds no dependencies of its own (or only depends on singletons) and than becomes a singleton. The MyLookup can than be 'downgraded' to being Scoped and pass the runtime data on to its singleton dependency that does the caching. I would have loved showing you an example of this, but your question needs more details in order to do this.

But if you want to leave the MyLookup a singleton, there are definitely ways to do this. For instance, you can wrap a single operation inside a scope. Example:

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
    {
        _cache = memoryCache;
        _scopeFactory = scopeFactory;
    }

    private List<string> QueryNamesFromDB()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            var allNames = context.Persons.Select(e => e.Name).Distinct().ToList<string>();
            return allSInames;
        }
    }
}

In this example, the MyLookup is injected with a IServiceScopeFactory. This allows the creation (and destruction) of an IServiceScope in a single call. Downside of this approach is that MyLookup now requires a dependency on the DI Container. Only classes that are part of the Composition Root should be aware of the existence of the DI Container.

So instead, a common approach is to inject a Func<MyDbContext> dependency. But this is actually pretty hard with MS.DI, because when you try this, the factory comes scoped to the root container, while your DbContext always needs to be scoped. There are ways around this, but I'll not go into those, due to time constrains from my side, and because that would just complicate my answer.

To separate the dependency on the DI Container from your business logic, you would either have to:

  • Move this complete class inside your Composition Root
  • or split the class into two to allow the business logic to be kept outside the Composition Root; you might for instance achieve this using sub classing or using composition.