[C#] Fixing Duplicate API Requests by Idempotency-Key

ASP.NET Core

First, ensure that MemoryCache is available by adding it to your project. In Startup.cs or Program.cs, configure memory cache.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMemoryCache(); // Register Memory Cache
    services.AddControllers();
}


This middleware will check if an incoming request has an Idempotency-Key and ensure that duplicate requests with the same key are rejected.

public class IdempotencyMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IMemoryCache _cache;


    public IdempotencyMiddleware(RequestDelegate next, IMemoryCache cache)
    {
        _next = next;
        _cache = cache;
    }


    public async Task InvokeAsync(HttpContext context)
    {
        if (!context.Request.Headers.TryGetValue("Idempotency-Key", out var idempotencyKey))
        {
            context.Response.StatusCode = 400; // Bad Request
            await context.Response.WriteAsync("Idempotency-Key is required.");
            return;
        }


        // Check if the key exists in memory
        if (_cache.TryGetValue(idempotencyKey, out _))
        {
            context.Response.StatusCode = 409; // Conflict
            await context.Response.WriteAsync("Duplicate request.");
            return;
        }


        // Store the key in cache to prevent duplicate processing
        _cache.Set(idempotencyKey, true, TimeSpan.FromMinutes(10)); // Cache for 10 minutes


        // Proceed to the next middleware or controller
        await _next(context);
    }
}


In Startup.cs or Program.cs, register the middleware in the request pipeline.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseMiddleware<IdempotencyMiddleware>(); // Add the middleware to the pipeline


    app.UseRouting();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}


Here’s a sample API controller that can use this Idempotency middleware.

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateOrder([FromBody] OrderRequest orderRequest)
    {
        // Simulate order creation logic
        return Ok(new { Message = "Order created successfully." });
    }
}


public class OrderRequest
{
    public string ProductId { get; set; }
    public int Quantity { get; set; }
}


Explanation:

  1. Middleware: The IdempotencyMiddleware checks for an Idempotency-Key in the request headers. If it’s missing or duplicated, the request is rejected with an appropriate HTTP status code.
  2. Cache: The key is stored in MemoryCache for a short period (e.g., 10 minutes). This prevents duplicate requests within that time window.
  3. Flexibility: You can adjust the expiration time (TimeSpan.FromMinutes(10)) based on how long you want to keep requests idempotent.


ASP.NET Framework

In an ASP.NET Framework project, you can use MemoryCache from System.Runtime.Caching. You’ll need to add this in your Global.asax.cs file for configuration.

using System;
using System.Runtime.Caching;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;


namespace YourAppNamespace
{
    public class MvcApplication : System.Web.HttpApplication
    {
        public static ObjectCache Cache = MemoryCache.Default;


        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }
    }
}


In ASP.NET Framework, you can use Action Filters to implement the Idempotency mechanism.

using System;
using System.Runtime.Caching;
using System.Web.Mvc;


namespace YourAppNamespace.Filters
{
    public class IdempotencyFilter : ActionFilterAttribute
    {
        private readonly ObjectCache _cache;


        public IdempotencyFilter()
        {
            _cache = MemoryCache.Default;
        }


        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.HttpContext.Request;
            var response = filterContext.HttpContext.Response;


            // Check for Idempotency-Key in the headers
            var idempotencyKey = request.Headers["Idempotency-Key"];
            if (string.IsNullOrEmpty(idempotencyKey))
            {
                response.StatusCode = 400; // Bad Request
                filterContext.Result = new ContentResult { Content = "Idempotency-Key is required." };
                return;
            }


            // Check if the key exists in cache
            if (_cache.Contains(idempotencyKey))
            {
                response.StatusCode = 409; // Conflict
                filterContext.Result = new ContentResult { Content = "Duplicate request." };
                return;
            }


            // Store the key in cache to prevent duplicate processing
            _cache.Add(idempotencyKey, true, DateTimeOffset.UtcNow.AddMinutes(10)); // Cache for 10 minutes
        }
    }
}


Now, you can apply the IdempotencyFilter to your API controller or action method.

using System.Web.Mvc;
using YourAppNamespace.Filters;


namespace YourAppNamespace.Controllers
{
    public class OrdersController : Controller
    {
        // Apply IdempotencyFilter at the action level
        [HttpPost]
        [IdempotencyFilter]
        public ActionResult CreateOrder(OrderRequest orderRequest)
        {
            // Simulate order creation logic
            return Json(new { Message = "Order created successfully." });
        }
    }


    public class OrderRequest
    {
        public string ProductId { get; set; }
        public int Quantity { get; set; }
    }
}


You can also register this filter globally in FilterConfig.cs if you want it applied to all your controllers.

using System.Web.Mvc;
using YourAppNamespace.Filters;


public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new IdempotencyFilter());
    }
}


Explanation:

  1. MemoryCache: We use MemoryCache from System.Runtime.Caching to store the Idempotency-Key for 10 minutes, preventing duplicate calls within that period.
  2. Idempotency Filter: The IdempotencyFilter checks the request headers for an Idempotency-Key, ensures the request is not duplicated, and stores the key in the cache.
  3. Controller: The controller action (CreateOrder) uses the IdempotencyFilter to ensure that duplicate calls with the same key are blocked.