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

Blazor Server is a stateful app framework. The app maintains an ongoing connection to the server, and the user's state is held in the server's memory in a circuit. One example of user state is data held in dependency injection (DI) service instances that are scoped to the circuit. The unique application model that Blazor Server provides requires a special approach to use Entity Framework Core.

Note

This article addresses EF Core in Blazor Server apps. Blazor WebAssembly apps run in a WebAssembly sandbox that prevents most direct database connections. Running EF Core in Blazor WebAssembly is beyond the scope of this article.

Sample app

The sample app was built as a reference for Blazor Server apps that use EF Core. The sample app includes a grid with sorting and filtering, delete, add, and update operations. The sample demonstrates use of EF Core to handle optimistic concurrency.

View or download sample code (how to download)

The sample uses a local SQLite database so that it can be used on any platform. The sample also configures database logging to show the SQL queries that are generated. This is configured in appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

The grid, add, and view components use the "context-per-operation" pattern, where a context is created for each operation. The edit component uses the "context-per-component" pattern, where a context is created for each component.

Note

Some of the code examples in this topic require namespaces and services that aren't shown. To inspect the fully working code, including the required @using and @inject directives for Razor examples, see the sample app.

Database access

EF Core relies on a DbContext as the means to configure database access and act as a unit of work. EF Core provides the AddDbContext extension for ASP.NET Core apps that registers the context as a scoped service by default. In Blazor Server apps, scoped service registrations can be problematic because the instance is shared across components within the user's circuit. DbContext isn't thread safe and isn't designed for concurrent use. The existing lifetimes are inappropriate for these reasons:

  • Singleton shares state across all users of the app and leads to inappropriate concurrent use.
  • Scoped (the default) poses a similar issue between components for the same user.
  • Transient results in a new instance per request; but as components can be long-lived, this results in a longer-lived context than may be intended.

The following recommendations are designed to provide a consistent approach to using EF Core in Blazor Server apps.

  • By default, consider using one context per operation. The context is designed for fast, low overhead instantiation:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Use a flag to prevent multiple concurrent operations:

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Place operations after the Loading = true; line in the try block.

  • For longer-lived operations that take advantage of EF Core's change tracking or concurrency control, scope the context to the lifetime of the component.

New DbContext instances

The fastest way to create a new DbContext instance is by using new to create a new instance. However, there are several scenarios that may require resolving additional dependencies. For example, you may wish to use DbContextOptions to configure the context.

The recommended solution to create a new DbContext with dependencies is to use a factory. EF Core 5.0 or later provides a built-in factory for creating new contexts.

The following example configures SQLite and enables data logging. The code uses an extension method (AddDbContextFactory) to configure the database factory for DI and provide default options:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

The factory is injected into components and used to create new instances. For example, in Pages/Index.razor:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Note

Wrapper is a component reference to the GridWrapper component. See the Index component (Pages/Index.razor) in the sample app.

New DbContext instances can be created with a factory that allows you to configure the connection string per DbContext, such as when you use ASP.NET Core's Identity model:

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

Scope to the component lifetime

You may wish to create a DbContext that exists for the lifetime of a component. This allows you to use it as a unit of work and take advantage of built-in features, such as change tracking and concurrency resolution. You can use the factory to create a context and track it for the lifetime of the component. First, implement IDisposable and inject the factory as shown in Pages/EditContact.razor:

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

The sample app ensures the context is disposed when the component is disposed:

public void Dispose()
{
    Context.Dispose();
}

Finally, OnInitializedAsync is overridden to create a new context. In the sample app, OnInitializedAsync loads the contact in the same method:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

Enable sensitive data logging

EnableSensitiveDataLogging includes application data in exception messages and framework logging. The logged data can include the values assigned to properties of entity instances and parameter values for commands sent to the database. Logging data with EnableSensitiveDataLogging is a security risk, as it may expose passwords and other personally identifiable information (PII) when it logs SQL statements executed against the database.

We recommend only enabling EnableSensitiveDataLogging for development and testing:

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Blazor Server is a stateful app framework. The app maintains an ongoing connection to the server, and the user's state is held in the server's memory in a circuit. One example of user state is data held in dependency injection (DI) service instances that are scoped to the circuit. The unique application model that Blazor Server provides requires a special approach to use Entity Framework Core.

Note

This article addresses EF Core in Blazor Server apps. Blazor WebAssembly apps run in a WebAssembly sandbox that prevents most direct database connections. Running EF Core in Blazor WebAssembly is beyond the scope of this article.

Sample app

The sample app was built as a reference for Blazor Server apps that use EF Core. The sample app includes a grid with sorting and filtering, delete, add, and update operations. The sample demonstrates use of EF Core to handle optimistic concurrency.

View or download sample code (how to download)

The sample uses a local SQLite database so that it can be used on any platform. The sample also configures database logging to show the SQL queries that are generated. This is configured in appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  }
}

The grid, add, and view components use the "context-per-operation" pattern, where a context is created for each operation. The edit component uses the "context-per-component" pattern, where a context is created for each component.

Note

Some of the code examples in this topic require namespaces and services that aren't shown. To inspect the fully working code, including the required @using and @inject directives for Razor examples, see the sample app.

Database access

EF Core relies on a DbContext as the means to configure database access and act as a unit of work. EF Core provides the AddDbContext extension for ASP.NET Core apps that registers the context as a scoped service by default. In Blazor Server apps, this can be problematic because the instance is shared across components within the user's circuit. DbContext isn't thread safe and isn't designed for concurrent use. The existing lifetimes are inappropriate for these reasons:

  • Singleton shares state across all users of the app and leads to inappropriate concurrent use.
  • Scoped (the default) poses a similar issue between components for the same user.
  • Transient results in a new instance per request; but as components can be long-lived, this results in a longer-lived context than may be intended.

The following recommendations are designed to provide a consistent approach to using EF Core in Blazor Server apps.

  • By default, consider using one context per operation. The context is designed for fast, low overhead instantiation:

    using var context = new MyContext();
    
    return await context.MyEntities.ToListAsync();
    
  • Use a flag to prevent multiple concurrent operations:

    if (Loading)
    {
        return;
    }
    
    try
    {
        Loading = true;
    
        ...
    }
    finally
    {
        Loading = false;
    }
    

    Place operations after the Loading = true; line in the try block.

  • For longer-lived operations that take advantage of EF Core's change tracking or concurrency control, scope the context to the lifetime of the component.

New DbContext instances

The fastest way to create a new DbContext instance is by using new to create a new instance. However, there are several scenarios that may require resolving additional dependencies. For example, you may wish to use DbContextOptions to configure the context.

The recommended solution to create a new DbContext with dependencies is to use a factory. The sample app implements its own factory in Data/DbContextFactory.cs.

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider;
        }

        public TContext CreateDbContext()
        {
            if (provider == null)
            {
                throw new InvalidOperationException(
                    $"You must configure an instance of IServiceProvider");
            }

            return ActivatorUtilities.CreateInstance<TContext>(provider);
        }
    }
}

In the preceding factory:

The following example configures SQLite and enables data logging. The code uses an extension method to configure the database factory for DI and provide default options:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));

The factory is injected into components and used to create new instances. For example, in Pages/Index.razor:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

Note

Wrapper is a component reference to the GridWrapper component. See the Index component (Pages/Index.razor) in the sample app.

New DbContext instances can be created with a factory that allows you to configure the connection string per DbContext, such as when you use [ASP.NET Core's Identity model])(xref:security/authentication/customize_identity_model):

services.AddDbContextFactory<ApplicationDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

services.AddScoped<ApplicationDbContext>(p => 
    p.GetRequiredService<IDbContextFactory<ApplicationDbContext>>()
    .CreateDbContext());

Scope to the component lifetime

You may wish to create a DbContext that exists for the lifetime of a component. This allows you to use it as a unit of work and take advantage of built-in features, such as change tracking and concurrency resolution. You can use the factory to create a context and track it for the lifetime of the component. First, implement IDisposable and inject the factory as shown in Pages/EditContact.razor:

@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory

The sample app ensures the context is disposed when the component is disposed:

public void Dispose()
{
    Context.Dispose();
}

Finally, OnInitializedAsync is overridden to create a new context. In the sample app, OnInitializedAsync loads the contact in the same method:

protected override async Task OnInitializedAsync()
{
    Busy = true;

    try
    {
        Context = DbFactory.CreateDbContext();
        Contact = await Context.Contacts
            .SingleOrDefaultAsync(c => c.Id == ContactId);
    }
    finally
    {
        Busy = false;
    }

    await base.OnInitializedAsync();
}

In the preceding example:

  • When Busy is set to true, asynchronous operations may begin. When Busy is set back to false, asynchronous operations should be finished.
  • Place additional error handling logic in a catch block.

Enable sensitive data logging

EnableSensitiveDataLogging includes application data in exception messages and framework logging. The logged data can include the values assigned to properties of entity instances and parameter values for commands sent to the database. Logging data with EnableSensitiveDataLogging is a security risk, as it may expose passwords and other personally identifiable information (PII) when it logs SQL statements executed against the database.

We recommend only enabling EnableSensitiveDataLogging for development and testing:

#if DEBUG
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
        .EnableSensitiveDataLogging());
#else
    services.AddDbContextFactory<ContactContext>(opt =>
        opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif

Additional resources