Optimistic Concurrency Management in ASP.NET Core Application Using Hypermedia

4 minute read

In the world of REST API, Hypermedia means adding links and important details directly into the API response. This helps clients use that information for more actions with the API.

In a previous article here, I discussed various ways to deal with concurrency. Optimistic concurrency is a common method for handling such situations. In this article, I’ll show a simple way to manage concurrency in ASP.NET using hypermedia.

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 Employee in Models folder

  • Employee class to store Employee Information
  • In the following class RowVersion property is used to track the updated version of the record.

Employee.cs

namespace ConcurrencyHandling.API.Models
{
    public class Employee
    {
        public int EmployeeID { get; set; }

        public string Name { get; set; }

        public decimal Salary { get; set; }

        public Guid RowVersion { 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 ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }
        public DbSet<Employee>? Employee { get; set; }
    }
}

Step 5: Add connection string in appsettings.json file

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
"ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=ConcurrencyHyperMediaDB;User Id=sa;Password=MyPassword;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 EmployeesController in Controllers folder.

EmployeesController.cs


using ConcurrencyHandling.API.Data;
using ConcurrencyHandling.API.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace ConcurrencyHandling.API.Controllers
{

    // Concurrency check using hypermedia
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        private readonly ApplicationDbContext _context;

        public EmployeesController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: api/Employees
        [HttpGet]
        public async Task<ActionResult<IEnumerable<Employee>>> GetEmployee()
        {
            if (_context.Employee == null)
            {
                return NotFound();
            }
            return await _context.Employee.ToListAsync();
        }

        // GET: api/Employees/5
        [HttpGet("{id}")]
        public async Task<ActionResult<Employee>> GetEmployee(int id)
        {
            if (_context.Employee == null)
            {
                return NotFound();
            }
            var employee = await _context.Employee.FindAsync(id);

            if (employee == null)
            {
                return NotFound();
            }

            return Ok(new
            {
                id = employee.EmployeeID,
                name = employee.Name,
                salary = employee.Salary,
                rowVersion = employee.RowVersion,
                links = new[]
                {
                    new { rel = "edit", href = $"/employees/{id}/{employee.RowVersion}", method ="PUT" },
                    new { rel = "delete", href = $"/employees/{id}/{employee.RowVersion}", method ="DELETE" }
                }
            });

            //employee;
        }

        // PUT: api/Employees/5
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPut("{id}")]
        public async Task<IActionResult> PutEmployee(int id, string version, Employee employee)
        {
            if (id != employee.EmployeeID)
            {
                return BadRequest();
            }

            var existingEmployee = await _context.Employee.FindAsync(id);
            if (existingEmployee == null)
            {
                return NotFound();
            }

            var currentRowVersion = existingEmployee.RowVersion.ToString();

            if (currentRowVersion != version)
            {
                return Conflict();
            }

            _context.Entry(existingEmployee).CurrentValues.SetValues(employee);
            existingEmployee.RowVersion = Guid.NewGuid();
            _context.Entry(existingEmployee).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw;
            }

            return NoContent();
        }

        // POST: api/Employees
        // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
        [HttpPost]
        public async Task<ActionResult<Employee>> PostEmployee(Employee employee)
        {
            if (_context.Employee == null)
            {
                return Problem("Entity set 'ApplicationDbContext.Employee'  is null.");
            }
            employee.RowVersion = Guid.NewGuid();
            _context.Employee.Add(employee);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetEmployee", new { id = employee.EmployeeID }, employee);
        }

        // DELETE: api/Employees/5
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteEmployee(int id, string version)
        {
            if (_context.Employee == null)
            {
                return NotFound();
            }
            var employee = await _context.Employee.FindAsync(id);
            if (employee == null)
            {
                return NotFound();
            }

            if (employee.RowVersion.ToString() != version)
            {
                return Conflict();
            }

            _context.Employee.Remove(employee);
            await _context.SaveChangesAsync();

            return NoContent();
        }
    }
}

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 PostEmployee action using swagger. Sample example for post data.
{
  "employeeID": 0,
  "name": "Tahiya Hasan",
  "salary": 1000000
}
  • Now, let’s retrieve the record using the GET method. You will get the following response.
{
  "id": 1,
  "name": "Tahiya Hasan",
  "salary": 1000000,
  "rowVersion": "8568375c-cfa5-4ec2-a2ab-b06184f424ad",
  "links": [
    {
      "rel": "edit",
      "href": "/employees/1/8568375c-cfa5-4ec2-a2ab-b06184f424ad",
      "method": "PUT"
    },
    {
      "rel": "delete",
      "href": "/employees/1/8568375c-cfa5-4ec2-a2ab-b06184f424ad",
      "method": "DELETE"
    }
  ]
}
  • Modify or delete the record by using either the provided URL or the specified version number within the URL. In the backend, the version number from the URL will be compared with the existing version number in the action method as outlined in the controller code. The action will only be executed if the version numbers from the URL and the existing record match.

Source code