Hi, I'm developing a small application that uses ASP.NET Core as backend and Vue.js as frontend. As database access I chose Entity Framework Core for SQL Server. My doubt is about the ideal way to handle this case: Given 2 or more requests at the same time from multiple clients how to make the office seat reservation succeed only for one request.
The context is the following:
SELECT COUNT(*) FROM Reservations WHERE OfficeId = x and Date = 'y';
(with x and y being the two request parameters)val_query < office_capacity
. In this way I know there is room at least for one reservation.Given a single available seat, if two requests to book an office seat on the same date are processed at the same time, and both read that there is room for the reservation, they will insert the row in the Reservations table even if in theory there would only be room for one.
Here are a couple solutions I've been thinking about:
As for the second option, when a request inserts successfully a reservation, the readings of subsequent requests will take it into consideration and if there are no more places, they will not perform the insertion. If, on the other hand, more requests read the same data at the same time, they will perform the insertion, but at the second reading they will be able to check the violation of a constraint and remove the row in Reservations. In this last scenario, the available seat has not been assigned to any request, but at least the constraint of the maximum number of seats in an office is respected.
My question is whether there is a design pattern or best practice to solve the problem described above.
Database transactions are your friend here. However, the default transaction isolation level of ReadCommitted
will not work for what you need. It does not guarantee that the data you queried - whether there is a reservation for the room - will not change until your work is done; it only guarantees that the data you received is from committed transactions. By using the Serializable
isolation level, you can guarantee that no competing reservations will be added until the process with the transaction completes its work (of checking availability and then adding a reservation).
This documentation contains examples of how to use a transaction with your Entity Framework context. This code should give you the general idea:
// start a transaction with the higher isolation level
using (var dbContextTransaction = context.Database.BeginTransaction(System.Data.IsolationLevel.Serializable))
{
try
{
// run your query to ensure no competing reservation exists
// use AsNoTracking to ensure the query runs against your database in the transaction context
context.Reservations.AsNoTracking()...;
// if it is valid, insert your reservations
context.Reservations.Add(...);
// run the insert statement
context.SaveChanges();
// commit the transaction
dbContextTransaction.Commit();
}
catch
{
// roll back the transaction if anything went wrong so that your database isn't locked
dbContextTransaction.Rollback();
throw;
}
}