DEV Community

Cover image for DBContext, System.InvalidOperationException, and mouse clicking speed demons.
Matt Szczerba
Matt Szczerba

Posted on • Updated on

DBContext, System.InvalidOperationException, and mouse clicking speed demons.

Intro

So you’ve dipped your toes into Blazor and want to see what all the hype is about. By now, you're probably aware that Blazor offers incredible frontend performance and represents a huge paradigm shift for .NET.

Long gone are the days of page reloads and boring UIs. This positions Blazor uniquely among the many exceptional frameworks that are available today. Consider this, Blazor enables UIs that compete with some of best web frameworks, and has all of the power of .NET on the backend… at least to me, that's really exciting. I’ll cover the benefits of using Blazor and compare it to some modern frameworks in an upcoming post.

Anyways, onto what you’re here for. You’re using a single DBContext and got a little over-zealous with the ol’ mouse click and ran into the following error message.

System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

You’ve ensured that all of your methods are using async / await and you’ve exhausted the stack overflow suggestions of setting your services to scoped or transient. The reason you’re still running into this is simply because the implementation of DbContext isn’t thread safe. More reading here.

Alright just get to it

This post contains a tutorial on how to convert an existing service to make use of a DBContextFactory but also a summary at the bottom that gets right to it without the fluff. Pick your poison.

Tutorial

To get started, I’ve created a new Blazor Server project using .NET 6. For the purposes of this tutorial, I’ll make use of the existing WeatherForecastService and FetchData boilerplate files. I will also be using an Azure SQL Server database, however, this is not a requirement and should not add any additional steps for other databases. Required EF packages below.

Include="EntityFramework" Version="6.4.4"
Include="Microsoft.EntityFrameworkCore" Version="6.0.3"
Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.3"
Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.3"

First, I’ve updated my appsettings.json and added the connection strings for my Azure db. At this stage you should ensure that you are able to connect to your database before proceeding.

Next, I’m going to create a new file in the Data folder called ExampleContext.cs. This will be our DB Context.

ExampleDBContext

Let's flip over to program.cs (If you’re on an older version of .NET, this will be done in startup.cs) and we’ll add our DBContext. Additionally, we will update our WeatherForecastService to a scoped service (line 13).

program/startup

Sidenote: I made some minor changes to WeatherForecast.cs. We’re going to add the ForecastId key, and also change TemperatureF from a computed property to an int.

WeatherForecast.cs

Now that we’ve done that we’re able to scaffold our database and create the WeatherForecast table.

Note: If you do not have the dotnet tool installed, run this command dotnet tool install --global dotnet-ef

dotnet tool

Next, we’re going to create our first migration. Build the solution using dotnet build. Once the build succeeds, run the command dotnet ef migrations add CreateWeatherForecast.

create migration

If you receive the following error, cd into your project directory and try again.

No project was found. Change the current working directory or use the --project option.

Now, we’ll migrate our database using the dotnet ef database update command.

push migration

You should now be able to see the WeatherForecast table in your database.

Let's make some changes to the WeatherForecastService. First, we’ll need to add an ExampleContext field. We will also need to add a WeatherForecastService constructor with an ExampleContext parameter.

Next, we’ll alter the GetForecastAsync method to fetch the forecasts from the database. (This will be empty for now).

Finally, we will add an InsertForecastAsync method so that we can add new forecasts to our WeatherForecast table. This will also give us the opportunity to see the shortcomings of a traditional DbContext.

WeatherForecastServiceInsert

Now we'll go to our FetchData.razor file. First, let's add a new button that we’ll use to insert forecasts into our table (line 18).

Next, we’ll switch the forecasts array into a list due to the changes we made in the WeatherForecastService (line 44).

Now, add the InsertForecast function so that we can add some new forecasts to the table (Line 53-60). For the purposes of the demo, we’re just going to add the new forecast directly to the forecasts list, so it appears immediately in the table.

Fetchdata.razor

Launch the app and navigate to the fetch data tab (/fetchdata). You’ll see that we have an "Add new forecast" button at the top of the table. What happens if we click it rapidly?

insertBroken

oof

Let’s look into the error a bit more

System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

Well, let's see what Microsoft has to say about this.

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.

Alright, so how do we fix this? By using a DBContextFactory! We’ll be able to instantiate an instance of DbContext that will be scoped to each Task in the WeatherForecastService. After each task is complete, that instance of DbContext will be disposed of.

And its super easy! First though, we’re just going to quickly add a delete button so we can see how effective this really is.

In FetchData.razor add a table header (line 26), new button (line 37), and stub out the DeleteForecast method (line 64).

Fetch Data Updated

Head back to WeatherForecastService.cs and add a DeleteForecast method just under the InsertForecast method.

WeatherForecastService.cs updated

Now let's fill out our stubbed DeleteForecast method in FetchData.razor.

Delete Function Fetch Data

Our table should now look like this. If we rapidly try and delete these forecasts, we’ll run into the same problem. Time to fix it.

with delete

Head back to program.cs (or startup.cs if you’re using an older version of .net) and just change AddDbContext, to AddDbContextFactory.

updated program/startup

Next, head back to your WeatherForecastService.cs file and we’ll make some changes.

First we’re going to need to initialize the IDbContextFactory field on line 7. Next, modify our constructor to make use of the dbContextFactory.

In each of our methods, we’re going to instantiate a new instance of DbContext that will only exist for the lifetime of our Task. Next, update all of the _exampleContext references so that our actions use the dbContext we just instantiated.

factory service

That’s it! You’re done, head back to fetch data and see it in action.

fixedgif

Wow such speed, much fast. You’re all set to create the blazingly fast blazor app of your dreams, you mouse clicking speed demon.

Summary

Yaya, cool tutorial bro. I’m frustrated and just want this to work.

program.cs (>= .NET 6) , startup.cs (< .NET 6)

Turn your existing DbContext into a DbContextFactory

updated program/startup

Instantiate a DbContextFactory field in each of your services. (line 7)

Throw it into your constructors.

Create a new instance of DbContext in each method. This will only exist for the lifetime of your task. Perform your actions with the newly created DbContext.

updated service

Donezo.

Code here.

This is my first time creating a tutorial so all feedback is appreciated! If anything is incorrect, or there is a better approach, please let me know!

Top comments (0)