.NET Nakama

Improving your .NET skills

.NET IdempotentAPI v2.0.0RC Supports Idempotency in a Cluster Environment

October 04, 2022 (~13 Minute Read)
IDEMPOTENCY FAULT TOLERANCE DISTRIBUTED SYSTEMS API DESIGN ASP.NET CORE OPEN-SOURCE

Introduction

Idempotency in Web APIs ensures that the API works correctly (as designed) even when consumers (clients) send the same request multiple times. To simplify the integration of Idempotency in an API project, we could use the IdempotentAPI open-source NuGet library. IdempotentAPI implements an ASP.NET Core attribute (filter) to handle the HTTP write operations (POST and PATCH) to affect only once for the given request data and idempotency key.

In February 2022, IdempotentAPI 1.0.0-RC-01 was released with many improvements. The community’s contribution plays a crucial role in the evolution of any open source project. The community found issues and provided fixes and ideas for improvements. So, I would like to thank everyone who helped the IdempotentAPI project to be evolved.

Now, the IdempotentAPI 2.0.0-RC-01 is available with bug fixes and with the support of idempotency in a Cluster Environment (i.e., a group of multiple server instances) using Distributed Locks 🎉✨. The complete journey of the IdempotentAPI is available in the CHANGELOG.md file.

In the following sections, we will see the complete features, details regarding the improvements, the available NuGet packages, and instructions to start using the IdempotentAPI library quickly.

Features

  • Simple: Support idempotency in your APIs easily with three simple steps 1️⃣2️⃣3️⃣.
  • 🔍 Validations: Performs validation of the request’s hash-key to ensure that the cached response is returned for the same combination of Idempotency Key and Request to prevent accidental misuse.
  • 🌍 Use it anywhere!: IdempotentAPI targets .NET Standard 2.0. So, we can use it in any compatible .NET implementation (.NET Framework, .NET Core, etc.). Click here to see the minimum .NET implementation versions that support each .NET Standard version.
  • Configurable: Customize the idempotency in your needs.
    • Configuration Options (see the GitHub repository for more details)
    • Logging Level configuration.
  • 🔧 Caching Implementation based on your needs.
    • 🏠 DistributedCache: A build-in caching based on the standard IDistributedCache interface.
    • 🦥 FusionCache: A high-performance and robust cache with an optional distributed 2nd layer and advanced features.
    • … or you could use your implementation 😉.
  • NEW ✳- 🔀 Support idempotency in a Cluster Environment (i.e., a group of multiple server instances) using Distributed Locks.

Improvement Details

Caching as Implementation Detail V2

In the IdempotentAPI v1.0.0, we have introduced a caching abstraction (IIdempotencyCache) to support other caching implementations, such as FusionCache. However, our needs have been expanded to abstract access to the cache and support distributed locks. In the following list, we can see the main abstractions:

  • IIdempotencyAccessCache in the IdempotentAPI.AccessCache project: Provide the IdempotentAPI a simple access caching method. So, it orchestrates how to access the cache. The default implementation (IdempotencyAccessCache) performs in-process and distributed access lock per idempotency key.
  • IIdempotencyCache in the IdempotentAPI.Cache.Abstractions project: Defines the caching methods required from the IdempotentAPI. So, we can provide multiple implementations for caching.
  • IDistributedAccessLockProvider in the IdempotentAPI.DistributedAccessLock.Abstractions project: Defines the methods to support distributed access locking. So, we can provide multiple implementations for distributed locking.

Cache Only Success Responses

The CacheOnlySuccessResponses attribute option is included (default value: true) to cache only 2xx HTTP responses.

[Idempotent(CacheOnlySuccessResponses = true)]
public class TestingIdempotentAPIController : ControllerBase
{
  // ...

}

Idempotency in a Cluster Environment

Refactoring has been performed to include additional abstractions and distinguish the Caching (IIdempotencyCache), Distributed Locks (IDistributedAccessLockProvider), and accessing of them (IIdempotencyAccessCache) to support idempotency in a cluster environment.

The issue when using multiple server instances is that we could retrieve the exact request (with the same Idempotency Key) in numerous servers concurrently. Thus, the applications will try to access the caching storage using the GetOrSet method, and all of them will set their value and continue with the controller action execution. The reason is that in the IdempotentAPI v1.0.0, the GetOrSet method has an in-process locking. So, it works as expected when using only one application instance (see more details here).

To overcome this issue in cluster environments, we perform a distributed lock per Idempotency Key (in the in-process lock). In this way, only one of the concurrent requests of the multiple server instances will execute the GetOrSet method. Thus, only one controller action will be executed per idempotency key. The rest of the requests will result in a 409 HTTP Conflict response.

A distributed lock performs a request to another system. Thus, we should use it only when it’s necessary because it will include a delay in our responses! So, the question that arises is how long we will wait to acquire the lock. Of course, the answer is that it depends on our use case. For that reason, the DistributedLockTimeoutMilli attribute option is included to configure the time the distributed lock will wait for the lock to be acquired (in milliseconds).

[Idempotent(DistributedLockTimeoutMilli = 2000)]
public class TestingIdempotentAPIController : ControllerBase
{
  // ...

}

Currently, we support the following two implementations. The next section will show how we can register the distributed lock implementations.

Update from v1.0.0 to v2.0.0

Before updating to v2.0.0, you should read the change log carefully because there are some breaking changes. The following list shows the essential parts when updating to v2.0.0.

  • ❗ IMPORTANT: We should register the IdempotentAPI Core services (services.AddIdempotentAPI()), which were unnecessary in v1.0.0.
  • ❗ IMPORTANT: The IdempotentAPI.Cache has been renamed to IdempotentAPI.Cache.Abstractions. So, you should remove the IdempotentAPI.Cache NuGet package and use the IdempotentAPI.Cache.Abstractions when needed.
  • ❗ The CacheOnlySuccessResponses attribute option is included with a default value of true to cache only 2xx HTTP responses. This behavior is changed from v1.0.0, which cache all responses.
  • ❗ The use of Distributed Locks is Optional. So, use it only when you want to support idempotency in a cluster environment.

Quick Start

Let’s see how we could use the NuGet packages in a Web API project. The IdempotentAPI can be installed via the NuGet UI or the NuGet package manager console:

PM> Install-Package IdempotentAPI -Version 2.0.0-rc-01

and, register the IdempotentAPI Core services:

services.AddIdempotentAPI();

Step 1.A: Register the Caching Storage

Storing-caching data is necessary for idempotency. Therefore, the IdempotentAPI library needs an implementation of the IIdempotencyCache to be registered in the Program.cs or Startup.cs file depending on the used style (.NET 6.0 or older). The IIdempotencyCache defines the caching storage service for the idempotency needs.

Currently, we support the following two implementations (see the following table). However, you can use your implementation 😉. Both implementations support the IDistributedCache either as primary caching storage (require its registration) or as secondary (optional registration).

Thus, we can define our caching storage service in the IDistributedCache, such as in Memory, SQL Server, Redis, NCache, etc. See the Distributed caching in the ASP.NET Core article for more details about the available framework-provided implementations.

  Support Concurrent Requests Primary Cache 2nd-Level Cache Advanced Features
IdempotentAPI.Cache.DistributedCache (Default) IDistributedCache
IdempotentAPI.Cache.FusionCache Memory Cache ✔(IDistributedCache)

Choice 1 (Default): IdempotentAPI.Cache.DistributedCache

Install the IdempotentAPI.Cache.DistributedCache via the NuGet UI or the NuGet package manager console.

// Register an implementation of the IDistributedCache.

// For this example, we are using a Memory Cache.

services.AddDistributedMemoryCache();
 
// Register the IdempotentAPI.Cache.DistributedCache.

services.AddIdempotentAPIUsingDistributedCache();

Choice 2: Registering: IdempotentAPI.Cache.FusionCache

Install the IdempotentAPI.Cache.FusionCache via the NuGet UI or the NuGet package manager console. To use the advanced FusionCache features (2nd-level cache, Fail-Safe, Soft/Hard timeouts, etc.), configure the FusionCacheEntryOptions based on your needs (for more details, visit the FusionCache repository).

// Register the IdempotentAPI.Cache.FusionCache.

// Optionally: Configure the FusionCacheEntryOptions.

services.AddIdempotentAPIUsingFusionCache();

💡 TIP: To use the 2nd-level cache, we should register an implementation for the IDistributedCache and register the FusionCache Serialization (NewtonsoftJson or SystemTextJson). For example, check the following code:

// Register an implementation of the IDistributedCache.

// For this example, we are using Redis.

services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "YOUR CONNECTION STRING HERE, FOR EXAMPLE:localhost:6379";
});
 
// Register the FusionCache Serialization (e.g. NewtonsoftJson).

// This is needed for the a 2nd-level cache.

services.AddFusionCacheNewtonsoftJsonSerializer();
 
// Register the IdempotentAPI.Cache.FusionCache.

// Optionally: Configure the FusionCacheEntryOptions.

services.AddIdempotentAPIUsingFusionCache();

Step 1.B (Optional): Register the Distributed Locks for Cluster Environment

Currently, we support the following two Distributed Lock implementations. However, you can use your implementation 😉.

Choice 1: None

If you do not need to support idempotency in a Cluster Environment, you do not have to register anything. So, skip this step 😉.

Choice 2: samcook/RedLock.net (Redis)

Install the IdempotentAPI.DistributedAccessLock.RedLockNet via the NuGet UI or the NuGet package manager console. The samcook/RedLock.net supports the Redis Redlock algorithm.

// Define the Redis endpoints:

List<DnsEndPoint> redisEndpoints = new List<DnsEndPoint>()
{
  new DnsEndPoint("localhost", 6379)
};
 
// Register the IdempotentAPI.DistributedAccessLock.RedLockNet:

services.AddRedLockNetDistributedAccessLock(redisEndpoints);

Choice 3: madelson/DistributedLock (Multiple Technologies)

Install the IdempotentAPI.DistributedAccessLock.MadelsonDistributedLock via the NuGet UI or the NuGet package manager console. The madelson/DistributedLock supports multiple technologies such as Redis, SqlServer, Postgres and many more.

// Register the distributed lock technology.

// For this example, we are using Redis.

var redicConnection = ConnectionMultiplexer.Connect("localhost:6379");
services.AddSingleton<IDistributedLockProvider>(_ => new RedisDistributedSynchronizationProvider(redicConnection.GetDatabase()));
 
// Register the IdempotentAPI.DistributedAccessLock.MadelsonDistributedLock

services.AddMadelsonDistributedAccessLock();

Step 2: Decorate Response Classes as Serializable

The response Data Transfer Objects (DTOs) need to be serialized before caching. For that reason, we will have to decorate the relative DTOs as [Serializable]. For example, see the code below.

using System;
 
namespace WebApi_3_1.DTOs
{
 [Serializable]
public class SimpleResponse
 {
    public int Id { get; set; }
    public string Message { get; set; }
    public DateTime CreatedOn { get; set; }
 }
}

Step 3: Set Controller Operations as Idempotent

In your Controller class, add the following using statement. Then choose which operations should be Idempotent by setting the [Idempotent()] attribute, either on the controller’s class or each action separately. The following two sections describe these two cases. First, however, we should define the Consumes and Produces attributes on the controller in both cases.

using IdempotentAPI.Filters;

Using the Idempotent Attribute on a Controller’s Class

By using the Idempotent attribute on the API controller’s class, all POST and PATCH actions will work as idempotent operations (requiring the IdempotencyKey header).

[ApiController]
[Route("[controller]")]
[Consumes("application/json")] // We should define this.

[Produces("application/json")] // We should define this.

[Idempotent(Enabled = true)]
public class SimpleController : ControllerBase
{
  // ...

}

Using the Idempotent Attribute on a Controller’s Action

By using the Idempotent attribute on each action (HTTP POST or PATCH), we can choose which of them should be Idempotent. In addition, we could use the Idempotent attribute to set different options per action.

[HttpPost]
[Idempotent(ExpireHours = 48)]
public IActionResult Post([FromBody] SimpleRequest simpleRequest)
{
  // ...

}

NuGet Packages

Package Name Description
IdempotentAPI The implementation of the IdempotentAPI library.
IdempotentAPI.AccessCache The access cache implementation of the IdempotentAPI project.
IdempotentAPI.Cache (Obsolete) renamed to IdempotentAPI.Cache.Abstractions Defines the caching abstraction(IIdempotencyCache) that IdempotentAPI is based.
IdempotentAPI.Cache.DistributedCache The default caching implementation, based on the standard IDistributedCache interface.
IdempotentAPI.Cache.FusionCache Supports caching via the FusionCache third-party library.
IdempotentAPI.DistributedAccessLock.Abstractions The distributed access lock definition of the IdempotentAPI project.
IdempotentAPI.DistributedAccessLock.RedLockNet The RedLockNet implementation for the definition of the IdempotentAPI.DistributedAccessLock.
IdempotentAPI.DistributedAccessLock.MadelsonDistributedLock The Madelson DistributedLock implementation for the definition of the IdempotentAPI.DistributedAccessLock.

Summary

The IdempotentAPI 2.0.0-RC-01 is available with bug fixes and with the support of idempotency in a Cluster Environment (i.e., a group of multiple server instances) using Distributed Locks 🎉✨. Several issues and improvements have been identified and implemented with the community’s contribution. So, I would like to thank @kvuong, @bernardiego, @MohamadTahir, and @Rast1234 for your support, ideas, source code, and time in improving the IdempotentAPI project.

Any help in coding, suggestions, questions, giving a GitHub Star, etc., are welcome 😉. If you are using this library, don’t hesitate to contact me. I would be happy to know your use case 😁.

If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!

Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.