1

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;
        }
    }
FoggyDay
  • 11,962
  • 4
  • 34
  • 48
  • 1
    You should definitely read about [Captive Dependencies](https://blog.ploeh.dk/2014/06/02/captive-dependency/). – Steven Mar 14 '20 at 19:37
  • 1
    And you should read about [DI Composition Models](https://blogs.cuttingedge.it/steven/posts/2019/di-composition-models-primer/). – Steven Mar 14 '20 at 19:37
  • 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 Mar 14 '20 at 19:50
  • @Steven - One of your links pointed to [this link](https://simpleinjector.readthedocs.io/en/latest/LifestyleMismatches.html), 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 Mar 14 '20 at 20:01
  • Why do you need your service to be a singleton in the first place? – Steven Mar 14 '20 at 20:13
  • Q: Why do I want my service to be a singleton in the first place? A: So I can do the (expensive!) lookup once, at program startup, and then use the results (saved in a memory cache) whenever I need; across any/all requests over the lifetime of the app Q: Do you have any alternative suggestions? – FoggyDay Mar 14 '20 at 20:31
  • You don't need it to be singleton. You need the method Rebuid to be called once. Having this object to be singleton does not prevent from calling method Rebuild several times. You may have it scoped but just call the method once. – Sergey L Mar 14 '20 at 20:38
  • It's my understanding that if I have it "scoped" then a) the lookup's constructor (with the memorycache dependency) gets called *each* time from *each* different session, and b) the memory cache itself needs to be rebuilt each time for each different session. Q: Is this incorrect? Q: How do I ensure the cache gets built just *once*, over the lifetime of the application, then *shared* each invocation? Sounds like a "singleton" to me :( – FoggyDay Mar 14 '20 at 20:44
  • It is my understanding that the `IMemoryCache` already is a singleton, which means that it will cache its data for as long as the application lives (or as long as you tell it to cache). The lifetime of its consumers is irrelevant to whether its data is kept alive or not. – Steven Mar 14 '20 at 21:00

1 Answers1

2

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.
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Beautiful. Special thanks for translating "theory" into a concrete example :) Your links - and this response - have given me much useful info. I'll let you know how it goes. FOLLOW ON QUESTION: Q: Suppose I make "MyLookup" scoped again: where would be a good place/what would be a good way to invoke "MyLookup.Rebuild()"? In Startup.Configure(), as in my example above? – FoggyDay Mar 14 '20 at 21:13
  • 1
    Upvoted the answer and also found similar answer here https://stackoverflow.com/questions/36332239/use-dbcontext-in-asp-net-singleton-injected-class – theSushil Mar 14 '20 at 21:26
  • @FoggyDay: "where would be a good place/what would be a good way to invoke "MyLookup.Rebuild()"?" That completely depends on what should trigger the rebuild. In case it is a timer, it should likely be the Composition Root. In case the trigger is a web request; not so much. – Steven Mar 14 '20 at 22:27
  • All I want to do is create a "lookup service": to build some data structures and save them in a memory cache during "startup", before any requests come in. That's a fairly straightforward thing to do in other environments. It seems problematic since .Net Core seems to want my cache, my DBContext, and the lookup "service" itself to all use DI :( Help. – FoggyDay Mar 14 '20 at 23:57