Exploring Concurrency and Concurrency Management in ASP.NET Core Applications with Entity Framework
Concurrency refers to the simultaneous execution of multiple tasks or processes within a system. In the context of software development, it often involves managing multiple operations concurrently to improve system efficiency and responsiveness.
Handling concurrency in software applications is crucial to prevent conflicts and ensure data consistency when multiple users or processes attempt to access and modify shared data simultaneously. Several techniques are employed to manage concurrency effectively:
Pessimistic Concurrency Control: Acquiring locks on data before allowing any modifications, ensuring exclusive access during the transaction.
Optimistic Concurrency Control: Allowing multiple transactions to proceed independently but checking for conflicts at the end, typically through versioning or timestamps, and handling conflicts accordingly.
Locking Mechanisms: Using locks to control access to shared resources and ensuring that only one process can modify data at a time.
Transaction Isolation: Employing transactional mechanisms to encapsulate a series of operations, ensuring they either all succeed or none at all, thus maintaining data integrity.
Conflict Resolution: Implementing strategies to resolve conflicts when they occur, such as merging changes, notifying users of conflicts, or rolling back transactions.
Understanding the nature of the application, the database system in use, and the specific concurrency requirements helps developers choose the most suitable approach for handling concurrency effectively. But, Optimistic Concurrency control mechanism is mostly used in the industry.
Optimistic concurrency handling using application managed token using entity framework
In this article, we will explore how to handle optimistic concurrency using application managed token using entity framework. In this case, the client application will send the token to the server with the request. The server will check the token with the database. If the token is valid, then the server will update the data and send the updated token to the client. If the token is invalid, then the server will send the error message to the client. The client will show the error message to the user and refresh the data from the server. Let’s move to the demo on how to handle optimistic concurrency using an application-managed token.
Tools and Technology Used
- ASP.net core Web API
- Visual C#
- Entity Framework
- SQL Server
Step 1: Create a asp.net core web api project name ConcurrencyHandling.API
Step 2: Install the following nuget packages in the project.
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
Step 3: Create a Model class name Booking
- Booking class to store Booking Information
- In the following class RecordVersion property is a Guid which is used to track the updated version of the record.
- ConcurrencyCheck attribute is specifies that this property participates in optimistic concurrency checking. This column is used in the WHERE clause of UPDATE and DELETE operation.
Booking.cs
using System.ComponentModel.DataAnnotations;
namespace ConcurrencyHandling.API.Models
{
public class Booking
{
public int BookingID { get; set; }
public string CustomerName { get; set; }
public int RoomID { get; set; }
public DateTime BookingDate { get; set; }
public DateTime CheckInDate { get; set; }
public DateTime CheckOutDate { get; set; }
// ConcurrencyCheck is specifies that this property participates in optimistic concurrency checking.
[ConcurrencyCheck]
public Guid RecordVersion { get; set; }
}
}
Step 4: Create a Context class name ApplicationDbContext in Data folder.
ApplicationDbContext.cs
using ConcurrencyHandling.API.Models;
using Microsoft.EntityFrameworkCore;
namespace ConcurrencyHandling.API.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Booking>? Booking { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
}
}
Step 5: Add connection string in appsettings.json file
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=ConcurrencyAppTokenDB;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
},
"AllowedHosts": "*"
}
Step 6: Configure Program class as follows.
Program.cs
using ConcurrencyHandling.API.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 7: Create a Controller class BookingsController in Controllers folder.
- In the BookingsController class that follows, the statement
booking.RecordVersion = Guid.NewGuid();
is employed within the PutBooking and PostBooking methods to update the record version. This updated version will then be utilized in subsequent requests, such as those for updating or deleting a record.
BookingsController.cs
using ConcurrencyHandling.API.Data;
using ConcurrencyHandling.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace ConcurrencyHandling.API.Controllers
{
// Concurrency check using application managed token
[Route("api/[controller]")]
[ApiController]
public class BookingsController : ControllerBase
{
private readonly ApplicationDbContext _context;
public BookingsController(ApplicationDbContext context)
{
_context = context;
}
// GET: api/Bookings
[HttpGet]
public async Task<ActionResult<IEnumerable<Booking>>> GetBooking()
{
if (_context.Booking == null)
{
return NotFound();
}
return await _context.Booking.ToListAsync();
}
// GET: api/Bookings/5
[HttpGet("{id}")]
public async Task<ActionResult<Booking>> GetBooking(int id)
{
if (_context.Booking == null)
{
return NotFound();
}
var booking = await _context.Booking.FindAsync(id);
if (booking == null)
{
return NotFound();
}
return booking;
}
// PUT: api/Bookings/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutBooking(int id, Booking booking)
{
if (id != booking.BookingID)
{
return BadRequest();
}
_context.Entry(booking).State = EntityState.Modified;
try
{
booking.RecordVersion = Guid.NewGuid();
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookingExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
// POST: api/Bookings
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Booking>> PostBooking(Booking booking)
{
if (_context.Booking == null)
{
return Problem("Entity set 'ApplicationDbContext.Booking' is null.");
}
booking.RecordVersion = Guid.NewGuid();
_context.Booking.Add(booking);
await _context.SaveChangesAsync();
return CreatedAtAction("GetBooking", new { id = booking.BookingID }, booking);
}
// DELETE: api/Bookings/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteBooking(int id, Booking booking)
{
if (_context.Booking == null)
{
return NotFound();
}
//var booking = await _context.Booking.FindAsync(id);
if (booking == null)
{
return NotFound();
}
_context.Booking.Remove(booking);
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookingExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
private bool BookingExists(int id)
{
return (_context.Booking?.Any(e => e.BookingID == id)).GetValueOrDefault();
}
}
}
Step 8: Create migration and update database
- Open Package Manager Console (PMC).
- Select the project name ConcurrencyHandling.API
- Run the following command in PMC
Add-Migration InitialCreate
This will create a new migration file named “InitialCreate” under the Migrations folder of your
- Run the following command in PMC
Update-Database
Step 9: Run the application and test concurrency
- Create a new record by PostBooking action using swagger. Sample example for post data.
{
"bookingID": 0,
"customerName": "Mahedee Hasan",
"roomID": 10,
"bookingDate": "2024-01-18T16:30:41.650Z",
"checkInDate": "2024-01-18T16:30:41.650Z",
"checkOutDate": "2024-01-18T16:30:41.650Z"
}
- Update the record using the PutBooking action in Swagger. A sample input is provided below. Meanwhile, open another tab and make an update to the record. Your record version will be altered. If you attempt to update the record using the previous record version, it will result in an exception due to concurrency. However, if no one else has updated it and you are using the correct record version, you can update the record without encountering any exceptions.
{
"bookingID": 1,
"customerName": "Humaira Hasan",
"roomID": 10,
"bookingDate": "2024-01-18T16:30:41.650Z",
"checkInDate": "2024-01-18T16:30:41.650Z",
"checkOutDate": "2024-01-18T16:30:41.650Z",
"recordVersion": "ed585f2c-720d-4bbb-a361-b059509d7f61"
}
- Delete the record using the DeleteBooking action in Swagger. A sample input is provided below. Meanwhile, open another tab and update the record. Your record version will be changed. If you attempt to delete the record using the previous record version, it will throw an exception due to concurrency. However, if no one else has updated it and you are using the correct record version, you can delete the record without encountering any exceptions.
{
"bookingID": 1,
"customerName": "Humaira Hasan",
"roomID": 10,
"bookingDate": "2024-01-18T16:30:41.650Z",
"checkInDate": "2024-01-18T16:30:41.650Z",
"checkOutDate": "2024-01-18T16:30:41.650Z",
"recordVersion": "ed585f2c-720d-4bbb-a361-b059509d7f61"
}
- When we execute update and delete request in the background the following query execute.
UPDATE [Booking] SET [BookingDate] = @p0, [CheckInDate] = @p1, [CheckOutDate] = @p2, [CustomerName] = @p3, [RecordVersion] = @p4, [RoomID] = @p5
OUTPUT 1
WHERE [BookingID] = @p6 AND [RecordVersion] = @p7;
- To view the generated SQL by Entity framework. Configre SQL Log in Program class as follows and you will see the the SQL output in Visual Studio Output window as like below.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.LogTo(Console.WriteLine, LogLevel.Information));