Solving Race Conditions With EF Core Optimistic Locking
Tags: efcore, concurrency, aspnetcore, dotnet
Published: 2023-10-14 10:50:25

---
title: Solving Race Conditions With EF Core Optimistic Locking
published: true
date: 2023-09-09 00:00:00 UTC
tags: efcore,concurrency,aspnetcore,dotnet
canonical_url: https://www.milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wau0dqewtj49h647sujs.png
---
How often do you think about concurrency conflicts when writing code?

You write the code for a new feature, confirm that it works, and call it a day.

But one week later, you find out you introduced a nasty bug because you didn't think about concurrency.

The most common issue is race conditions with two competing threads executing the same function. If you don't consider this during development, you introduce the risk of leaving the system in a corrupted state.

In this week's newsletter, I'll challenge you to spot the race condition in a method for reserving a booking. The business requirement is you can't have two overlapping reservations for the same dates.

And then, I'll show you how to solve this race condition using EF Core optimistic concurrency.

Let's dive in!

## What's Wrong With This Code?

There's a race condition hiding somewhere in this code snippet.

Can you see it?

```csharp
public Result<Guid> Handle(
    ReserveBooking command,
    AppDbContext dbContext)
{
    var user = dbContext.Users.GetById(command.UserId);
    var apartment = dbContext.Apartments.GetById(command.ApartmentId);
    var (startDate, endDate) = command;

    if (dbContext.Bookings.IsOverlapping(apartment, startDate, endDate))
    {
        return Result.Failure<Guid>(BookingErrors.Overlap);
    }

    var booking = Booking.Reserve(apartment, user, startDate, endDate);

    dbContext.Add(booking);

    dbContext.SaveChanges();

    return booking.Id;
}
```

The call to `IsOverlapping` is an optimistic check to see if there's an existing booking for the specified dates.

```csharp
if (dbContext.Bookings.IsOverlapping(apartment, startDate, endDate)) { }
```

If it returns `true`, we're trying to double-book the apartment. So we return a failure, and the method completes.

But if it returns `false`, we reserve a booking and call `SaveChanges` to persist the changes in the database.

And there lies the problem.

There's a chance for a concurrent request to pass the `IsOverlapping` check and attempt to reserve the booking. Without any concurrency control, both requests will succeed, and we will end up with an inconsistent state in the database.

So how can we solve this?

## Optimistic Concurrency With EF Core

The pessimistic concurrency approach acquires a lock for the data before modifying it. It's slower and causes competing transactions to be blocked until the lock is released. EF Core doesn't support this approach out of the box.

You can also solve this problem using optimistic concurrency with EF Core. It doesn't take any locks, but any data modifications will fail to save if the data has changed since it was queried.

To implement optimistic concurrency in EF Core, you need to configure a property as a _concurrency token_. It's loaded and tracked with the entity. When you call `SaveChanges`, EF Core will compare the value of the concurrency token to the value in the database.

Let's assume we're using SQL Server, which has a native `rowversion` column. The `rowversion` automatically changes when the row is updated, so it's a great option for a concurrency token.

To configure a `byte[]` property as a concurrency token you can decorate it with the `Timestamp` attribute. It will be mapped to a `rowversion` column in SQL Server.

```csharp
public class Apartment
{
    public Guid Id { get; set; }

    [Timestamp]
    public byte[] Version { get; set; }
}
```

I prefer a different approach because attributes pollute the entity.

You can do the same with the Fluent API. I will even use a shadow property to hide the concurrency token from the entity class.

```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Apartment>()
        .Property<byte[]>("Version")
        .IsRowVersion();
}
```

The exact configuration will differ based on the database you are using, so check the documentation.

## How Optimistic Concurrency Works In Practice

So here's what changes when we configure the concurrency token.

When loading the `Apartment` entity, EF will also load the concurrency token.

```sql
SELECT a.Id, a.Version
FROM Apartments a
WHERE a.Id = @p0
```

And when we call `SaveChanges`, the update statement will compare the concurrency token value with the one in the database:

```sql
UPDATE Apartments a
SET a.LastBookedOnUtc = @p0
WHERE a.Id = @p1 AND a.Version = @p2;
```

If the `rowversion` in the database changes, the number of updated rows will be `0`.

EF Core expects to update `1` row, so it will throw a `DbUpdateConcurrencyException`, which you need to handle.

## Handling Concurrency Exceptions

Now that you know how to use optimistic concurrency with EF Core, you can fix the previous code snippet.

If two concurrent requests pass the `IsOverlapping` check, only one can complete the `SaveChanges` call. The other concurrent request will run into a `Version` mismatch in the database and throw a `DbUpdateConcurrencyException`.

In case of a concurrency conflict, we need to add a `try-catch` statement to catch the `DbUpdateConcurrencyException`. How you handle the actual exception depends on your business requirements. And sometimes, [race conditions](https://go.particular.net/milanjovanovic/raceconditions) might not even exist.

```csharp
public Result<Guid> Handle(
    ReserveBooking command,
    AppDbContext dbContext)
{
    var user = dbContext.Users.GetById(command.UserId);
    var apartment = dbContext.Apartments.GetById(command.ApartmentId);
    var (startDate, endDate) = command;

    if (dbContext.Bookings.IsOverlapping(apartment, startDate, endDate))
    {
        return Result.Failure<Guid>(BookingErrors.Overlap);
    }

    try
    {
        var booking = Booking.Reserve(apartment, user, startDate, endDate);

        dbContext.Add(booking);

        dbContext.SaveChanges();

        return booking.Id;
    }
    catch (DbUpdateConcurrencyException)
    {
        return Result.Failure<Guid>(BookingErrors.Overlap);
    }
}
```

## When Should You Use Optimistic Concurrency?

Optimistic concurrency considers the best scenario is also the most probable one. It assumes conflicts between transactions will be infrequent and doesn't acquire locks on the data. This means your system can scale better because there is no blocking slowing down performance.

However, you must still expect concurrency conflicts and implement custom logic to handle them.

Optimistic concurrency is a good choice if your application doesn't expect many conflicts.

Another reason to use optimistic concurrency is when you can't hold an open connection to the database for the length of the transaction. This is required for pessimistic locking.

Hope this was helpful.

I'll see you next week!

* * *

**P.S. Whenever you're ready, there are 2 ways I can help you:**

1. [**Pragmatic Clean Architecture:**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. [**Join 1,000+ students here.**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting)

2. [**Patreon Community:**](https://www.patreon.com/milanjovanovic) Think like a senior software engineer with **access to the source code** I use in my YouTube videos and **exclusive discounts** for my courses. [**Join 860+ engineers here.**](https://www.patreon.com/milanjovanovic)