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:
Create a "lookup" that could be used by any consumer at any time, from any session, during the lifetime of the app.
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.
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;
}
}