# Solving Race Conditions With EF Core Optimistic Locking

> I'll challenge you to spot the race condition in a method for reserving a booking, where you can't have two overlapping reservations for the same dates. And then I'll show you how to solve it using EF Core optimistic concurrency.

Published: 2023-09-09. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/solving-race-conditions-with-ef-core-optimistic-locking

EF Core solves race conditions with optimistic concurrency, which takes no locks on the data.
You configure a property as a concurrency token, usually a SQL Server `rowversion` column, and EF Core adds it to the `WHERE` clause of the `UPDATE`.
If the value changed since you queried it, the save throws a `DbUpdateConcurrencyException`.

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 {9,18}
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**](https://milanjovanovic.tech/blog/a-clever-way-to-implement-pessimistic-locking-in-ef-core) 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`](https://learn.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql?view=sql-server-ver16) 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 {5}
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 {4,5}
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 {24}
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);
    }
}
```

If you're wondering how will this even work, here's the missing piece.
The `Booking.Reserve` method will update the `LastBookedOnUtc` property of the `Apartment` entity.

```csharp {3}
public static Booking Reserve(Apartment apartment, User user, DateTime startDate, DateTime endDate)
{
    apartment.LastBookedOnUtc = DateTime.UtcNow;

    return new Booking
    {
        Id = Guid.NewGuid(),
        Apartment = apartment,
        User = user,
        StartDate = startDate,
        EndDate = endDate
    };
}
```

When we call `SaveChanges`, this will cause an update to the `Apartment` entity, which will include the concurrency token check.
This ensures that if two requests try to update the same entity simultaneously, one will succeed while the other will fail with a `DbUpdateConcurrencyException`.

## 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**](https://milanjovanovic.tech/blog/introduction-to-locking-and-concurrency-control-in-dotnet-6) 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!

---

## Frequently asked questions

### What is optimistic concurrency in EF Core?

Optimistic concurrency takes no locks on the data. You configure a property as a concurrency token, and when you call SaveChanges, EF Core compares its value to the database. If the data changed since it was queried, the save fails with DbUpdateConcurrencyException.

### What is the difference between optimistic and pessimistic concurrency?

Pessimistic concurrency acquires a lock before modifying data, which blocks competing transactions until the lock is released. EF Core does not support it out of the box. Optimistic concurrency takes no locks and instead fails the save when it detects the data changed.

### How do I configure a concurrency token in EF Core?

Decorate a byte[] property with the Timestamp attribute, which maps to a rowversion column in SQL Server, or use the Fluent API with IsRowVersion. You can even configure it as a shadow property to keep the token out of the entity class.

### How does EF Core detect a concurrency conflict?

The UPDATE statement includes the concurrency token in the WHERE clause. If the rowversion changed in the database, zero rows are updated. EF Core expects one updated row, so it throws a DbUpdateConcurrencyException that you handle in a try-catch.

### When should I use optimistic concurrency?

Use it when conflicts between transactions are infrequent. Because there is no locking, the system scales better with no blocking. It also fits when you cannot hold an open database connection for the length of the transaction, which pessimistic locking requires.
