4

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.

Brian MacKay
  • 31,133
  • 17
  • 86
  • 125
  • Can't you just create factory for each service where you need to control lifetime? You might need to create some disposable wrapper around the service as well, or another way to dispose DbContext – Liero Dec 15 '20 at 07:11
  • @Liero I could, but it would look a lot like this solution except with way more code, right? Let me know if I'm missing something. – Brian MacKay Dec 15 '20 at 13:39
  • Might be better to ask this directly on the ASP.NET Core GitHub. – Ian Kemp Dec 15 '20 at 14:41
  • @IanKemp Maybe so. I wanted to see if people from the community had a good solution, can't imagine I managed to get here first. But if this doesn't get any traction I will move it over. – Brian MacKay Dec 15 '20 at 17:26
  • @Brian: It doesnt have to be a lot more code, factories can be done with generics. First of all, you should ask yourself what's wrong ServiceLocator. It's hiding dependencies which makes testing harder. Factories solves just that. – Liero Dec 16 '20 at 01:32
  • 2
    @Brian I wasn't saying you shouldn't ask it here, it's just that Blazor is still really new and niche, server-side even more so, so it's unlikely you will find many .NET devs working with it, thus limiting your answer possibilities. Plus this is a scenario that either the Blazor guys have thought of and hopefully have a solution; or haven't thought of, in which case getting it on their radar will be good for everyone in the long-term. – Ian Kemp Dec 16 '20 at 12:44
  • @Liero Solid points, thank you for expanding on that! – Brian MacKay Dec 16 '20 at 14:35
  • @IanKemp: I dont quite agree. This is exactly the kind of question that should be asked on stackoverflow first. It's not a bug, nor feature request and it can be answered by community. github is not a support forum. Btw, server side is older than webassembly, its there since. net core 3.0 – Liero Dec 17 '20 at 15:13
  • @Liero Yes, but community answers are far less likely to be correct or authoritative than if you go directly to the source i.e. GitHub. And since SO is far more likely to be picked up by search engines, therefore likely to reach far more people, I'd much prefer that any answers here be as correct as possible. While GitHub is more geared towards bug reports, there's no good reason why you shouldn't be able to ask a technical question like this there - especially a question about something that currently appears to be undocumented. – Ian Kemp Dec 17 '20 at 15:30
  • 1
    @Liero I did end up going in the factory direction. I also moved this over to github, along with some updates: https://github.com/dotnet/aspnetcore/discussions/28811 – Brian MacKay Dec 22 '20 at 21:35

0 Answers0