Author : HASSAN MD TAREQ | Updated : 2020/09/08

Problem with existing scopes

  • DbContext isn’t thread safe and isn’t designed for concurrent use
  • Due to stateful nature of blazor server, AddDbContext extension can be problematic because the instance is shared across components within the user’s circuit
  • 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.
  • Recommended Approach: Component Scope - create DbContext (using IDbContextFactory) in OnInitializedAsync() and disposing in Dispose()(implementing IDisposable)
    • Blazor server uses DbContext
    • Blazor WebAssembly uses API service in backend
  • Links:

AddDbContextFactory in Startup

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
	services.AddRazorPages();
	services.AddServerSideBlazor();

	// ... ... ...

	//var dbConnectionString = AzureUtil.GetKeyVaultSecretValue(keyVaultUri: Configuration[Constants.Azure.KeyVaultIdentifier], key: DB.ConnectionStringIdentifier);
	var dbConnectionString = Configuration["DBConnectionString"];
	
	services.AddDbContextFactory<EmailAddressDbContext>(options => options.UseSqlServer(dbConnectionString));

	// ... ... ...
}

DbContext Property and IDisposible in BaseComponent

Components/BaseComponent.cs

using Microsoft.AspNetCore.Components;
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using AutoMapper;
using System.Threading.Tasks;

public class BaseComponent: ComponentBase, IDisposable
{
	// ... ... ...

	[Inject]
	protected IDbContextFactory<FooDbContext> DbContextFactory { get; set; }
	
	protected FooDbContext DbContext { get; set; }

	[Inject]
	protected IMapper Mapper { get; set; }
	
	// ... ... ...

	protected void InitDependencies()
	{
		DbContext = DbContextFactory.CreateDbContext();
		
		// setup logger, other things
	}

	// ... ... ...
	
	public void Dispose()
	{
		DbContext?.Dispose(); // DbContext will be disposed automatically
	}
}

Using DbContext in component

Pages/delete.razor.cs

public class DeleteBase : BaseComponent
{
	[Parameter]
	public int Id { get; set; }

    // ... ... ...

	protected override async Task OnInitializedAsync()
	{
		InitDependencies();

		// ... ... ...
		
		// perform async operation
	}


	protected async Task PerformDeletion()
	{
		
		// ... ... ...

		try
		{
			var itemToDelete = await DbContext.Items.FindAsync(Id);

			// EF Core Operations
			DbContext.Items.Remove(itemToDelete);
			await DbContext.SaveChangesAsync();

			NavigationManager.NavigateTo("/items?deleted");
		}
		catch (Exception)
		{
			// handle exception, show error
		}
	}
	
	// ... ... ...
}