Anyone just starting out with Blazor Service Side with EF will come across this guidance:
https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-5.0
It makes two key points:
- Do not use DI to inject your DbContexts. Blazor pages are long-lived, and we need short-lived contexts. Long-lived contexts accumulate local data, which becomes stale and leads to unpredictable results.
- Instead, use the new DbContextFactory to materialize contexts as needed, and dispose the context when done.
It looks a bit like this:
using var db = DbContextFactory.CreateDbContext();
[...do work...]
And that works pretty well. When you have complex services, you might end up passing the context around as a parameter a bit more than is ideal, but it's fine.
So what's the problem?
I am integrating Blazor Server Side into an existing MVC project, and guess what? All those services inject the DbContext.
I wouldn't say it's a nightmare, but it's not a good dream either.
And because this is complicated, I will recap that this is not the way to go in Blazor Server Side. In MVC, this service is intended to be instantiated, used, and discarded at the speed of a request. In Blazor, the circuit could live for an hour or more, and the service would be along for the ride. The DbContext would go with it, accumulating stale data. And it would be Scoped, which exacerbates the problem even more.
So we don't want that.
What have you tried?
I've thought about a lot of ideas. The one that currently seems most reasonable is unfortunately a lot like Service Locator, which is often considered an anti-pattern.
Keeping in mind that my context is called KeystoneDb, this does the job even for complex graphs of objects:
public class BlazorServiceAdapter
{
private readonly IServiceProvider ServiceProvider;
/// <summary>
/// Create an instance of TService, using the supplied instance of KeystoneDb rather than
/// the one in the IoC container. Note that the service must have a constructor that takes KeystoneDb.
/// If there are multiple constructors, you can decorate the one you want to use with
/// [ActivatorUtilitiesConstructor].
/// </summary>
public TService GetService<TService>(KeystoneDb db)
{
return ActivatorUtilities.CreateInstance<TService>(ServiceProvider, db);
}
/// <summary>
/// Shortcut for injecting services, using the Service Locator (anti) pattern.
/// </summary>
public TService GetService<TService>()
{
return ServiceProvider.GetService<TService>();
}
public BlazorServiceAdapter(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
}
In practice it might look like this:
using var db = DbContextFactory.CreateDbContext();
var myService = BlazorServiceAdapter.GetService<SomeService>(db); // this db instance is used, instead of the one in the IoC container!
myService.doGoodThings(); // the service does its work and then falls out of scope.
I am on the fence about whether to actually use this... The Service Locator is an anti-pattern primarily for the reasons discussed here Is ServiceLocator an anti-pattern?, which don't seem very scary to me when balanced against having no other reasonable way forward. Also, this type of usage feels consistent with what the best practices are asking me to do with the context. It seems to me that by failing to create a way to get a DbContext from DI, they are actually pulling the entire project into Service Locator land.
So this question may be a tad subjective, but for the good of mankind, I would love to know if there is a best practice for this or if your team managed to avoid the problem when using Blazor with MVC services.