10

I am building a Blazor Server front end to an existing domain layer. This layer offers various injectable services to do modifications on the EF Core repository. For this, the services itself requests a DbContext from the (standard Microsoft) DI container. This works fine with regular MVC.NET/Razor pages with a scoped DbContext instance, but as documented, this is problematic with Blazor. In a Blazor Server app we’d want to use DbContextFactory to generate short-lived DbContext instances for operations instead.

It’s no problem to have both a DbContext and DbContextFactory in the same application, but I’m struggling to understand how to adapt my services. Or if I even need to? To illustrate, this is the current code:

My Page:

@page “/items”
@inject ItemService ItemService

// somewhere in the code
    ItemService.DoOperation(…)

My service

class ItemService
{
    public ItemService(MyDbContext myDbContext)
    {
        …
    }

    public bool DoOperation(…)
    {
        …
        _myDbContext.SaveChanges();
    }
}

Startup.cs:

            services.AddDbContext<MyDbContext>(options => …),
                contextLifetime: ServiceLifetime.Transient,
                optionsLifetime: ServiceLifetime.Singleton
                );

            services.AddDbContextFactory<MyDbContext>(options => …);

I’ve changed the lifetimes for DbContext according to the example given in this answer and so far I haven’t been able to create any issues, but I don’t fully understand the lifetime issues at play here. How can I engineer my service to play well in both a Blazor and a MVC/Razor Pages application in an obvious way?

Martijn
  • 1,662
  • 15
  • 21
  • 3
    Just an update: in EF Core 6, AddDbContextFactory now also registers DbContext so no need to inject them separately anymore. – Martijn Nov 21 '21 at 20:42

3 Answers3

7

In your typical MVC application, one request represents a single unit of work. The DbContext is generated as a scoped service and injected through the constructor. This is fine.

On the other hand, in Blazor Server one request no longer represents a single unit of work. The first request creates a circuit which means that any scoped service injected is going to have a lifetime as described here.

In Blazor Server apps, a unit of work is a SignalR message. (a button click for example that adds a new row to the database). Because of this, injecting your context directly is not the way to go.

That's why Blazor Server has IDbContextFactory<T>. Initialize it like this:

services.AddDbContextFactory<DbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("WebDB")));

In your Razor Components (tied to the Blazor app) you use it like so:

private readonly IDbContextFactory<DbContext> factory;

public Component(IDbContextFactory<DbContext> f)
{
    factory = f;
}

public void Click()
{
    using(DbContext cnt = factory.CreateDbContext())
    {
    // Your code here
    }
}

This is further explained here.

Documentation: ASP.NET Core Blazor Server with Entity Framework Core (EFCore).

Grizzlly
  • 486
  • 3
  • 14
  • 3
    Yes, so this is what I'm doing now for direct access of EF Core from Blazor. The problem lies in existing services I use that currently inject DbContext instead of DbContextFactory. This must be a common problem, yet I see nobody talking about it. – Martijn May 04 '21 at 06:42
  • [Polite] Why would you access the database directly through EF from a component? This ties the component to Blazor Server, which in my (personal) design book is a no no. I know the MS Docs link shows doing that, but .... – MrC aka Shaun Curtis May 04 '21 at 10:18
  • @MrCakaShaunCurtis I am just showing the official documentation. Personally, I would create a separate service for data access and inject that wherever I need it. All in all is up to the developer what method they use. – Grizzlly May 08 '21 at 08:21
  • @Grizzlly - Glad to hear. :-) – MrC aka Shaun Curtis May 09 '21 at 16:46
  • @martijn what is the correct way to use with services – Sebastian Jan 11 '22 at 22:58
  • @Sebastian with EF Core 6 this has now been improved https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/whatsnew#dbcontext-factory-improvements – Martijn Jan 14 '22 at 20:13
  • @Martijn so how you adapted your services for use in Blazor Server Side? you get a dbcontext from factory.CreateDbContext() in every method of your page and pass to your service (with a using to dispose)? or you do only once in OnInitialized/OnParameterSet ? And is UnitOfWork still working? I mean not only when I update a property of the Model, but eg. when I add an item to its ICollection (that should create a record in the many-to-many join table)? – SandroRiz Mar 18 '22 at 10:32
1

The problem is transitive: Your services rely on a should-be-scoped resource and that only works when you register those services as scoped as well. Which you can't.

The proper way is to rewrite your services to the DbContext per Operation model, and inject the DbContextFactory.

It looks like you already have a mixed model (with a SaveChanges per operation they are actually a UoW).

When you don't want to make those changes you could weasel your way out by registering the DbContext as Transient. That feels bad but it's designed to quickly release the underlying connection. So it's not the resource leak that it looks like.

H H
  • 263,252
  • 30
  • 330
  • 514
0

my practice: set dbcontext use database type

services.AddScoped<AuditableEntitySaveChangesInterceptor>();
    if (configuration.GetValue<bool>("UseInMemoryDatabase"))
    {
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseInMemoryDatabase("BlazorDashboardDb");
            options.EnableSensitiveDataLogging();
        });
    }
    else
    {
        services.AddDbContext<ApplicationDbContext>(options =>
        {
            options.UseSqlServer(
                  configuration.GetConnectionString("DefaultConnection"),
                  builder =>
                  {
                      builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName);
                      builder.EnableRetryOnFailure(maxRetryCount: 5,
                                                   maxRetryDelay: TimeSpan.FromSeconds(10),
                                                   errorNumbersToAdd: null);
                      builder.CommandTimeout(15);
                  });
            options.EnableDetailedErrors(detailedErrorsEnabled: true);
            options.EnableSensitiveDataLogging();
        });
        services.AddDatabaseDeveloperPageExceptionFilter();
    }

as we know DbContext should be transient lifetime in blazor server.

 services.AddTransient<IDbContextFactory<ApplicationDbContext>, BlazorContextFactory<ApplicationDbContext>>();
 services.AddTransient<IApplicationDbContext>(provider => provider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());

as sample, not change use DbContext:

 public class AddEditCustomerCommandHandler : IRequestHandler<AddEditCustomerCommand, Result<int>>
{
    private readonly IApplicationDbContext _context;
    private readonly IMapper _mapper;
    private readonly IStringLocalizer<AddEditCustomerCommandHandler> _localizer;
    public AddEditCustomerCommandHandler(
        IApplicationDbContext context,
        IStringLocalizer<AddEditCustomerCommandHandler> localizer,
        IMapper mapper
        )
    {
        _context = context;
        _localizer = localizer;
        _mapper = mapper;
    }
    }
neo.zhu
  • 19
  • 4
  • What is BlazorContextFactory and what is this line for: services.AddTransient, BlazorContextFactory>(); – Kirsten Jun 19 '23 at 02:44