.NET Nakama

Improving your .NET skills

.NET IdempotentAPI 1.0.0 Release Candidate

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

Introduction

A distributed system consists of multiple components located on different networked computers, which communicate and coordinate their actions by passing messages to one another from any system. Fault-tolerant applications can continue operating despite the system, hardware, and network faults of one or more components.

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 July 2021, we saw how the IdempotentAPI v0.1.0-beta in .NET Nakama (2021, July 4) provides an easy way to develop idempotent Web APIs in .NET Core. Since then, with the community’s help, several issues and improvements have been identified and implemented. The complete journey of the IdempotentAPI is available in the CHANGELOG.md file.

Now, the IdempotentAPI 1.0.0-RC-01 is available with many improvements 🎉✨. 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 😉

Improvement Details

Improving Concurrent Requests Handling

The standard IDistributedCache interface doesn’t support a command to GetOrSet a cached value with atomicity. However, it defines the Get and Set methods. In our previous implementation, we used these two methods without locking (i.e. without grouping into a single logical operation). As a result, we had an issue with concurrent requests with the same idempotency key. The problem was that the controller action could be executed multiple times.

As we can observe in Figure 1, this issue happens when the API Server receives a second request (with the same idempotency key) before we flag the first idempotency key as Inflight (i.e., execution in progress). Thus, racing conditions occur when setting idempotency key as Inflight.

The issue of executing the controller more than once when concurrent requests with the same idempotency key are performed.
Figure 1. - The issue of executing the controller more than once when concurrent requests with the same idempotency key are performed.

To overcome this issue, we defined the IIdempotencyCache interface and implemented the GetOrSet method, which performs a lock (locally) for each idempotency key (see code below). In Figure 2, we can see how we used the GetOrSet method to execute the controller action only once on concurrent requests with the same idempotency key.

The idea is to use GetOrSet method to set an Inflight object with a dynamic unique id per request when the Get method returns Null (it doesn’t have a value) as a single logical operation. The second call of the GetOrSet will wait for the first call to complete. Thus, only the execution that receives its unique id can continue with the execution of the controller action.

Concurrent requests with the same idempotency key, execute the controller action only once.
Figure 2. - Concurrent requests with the same idempotency key, execute the controller action only once.
public byte[] GetOrSet(
    string key,
    byte[] defaultValue,
    object? options = null,
    CancellationToken token = default)
{
    if (key is null)
    {
       throw new ArgumentNullException(nameof(key));
    }

    if (options is not null && options is not DistributedCacheEntryOptions)
    {
       throw new ArgumentNullException(nameof(options));
    }

    using (var valuelocker = new ValueLocker(key))
    {
       byte[] cachedData = _distributedCache.Get(key);
       if (cachedData is null)
       {
           _distributedCache.Set(key, defaultValue, (DistributedCacheEntryOptions?)options);
           return defaultValue;
       }
       else
       {
           return cachedData;
       }
    }
}

Caching as Implementation Detail

To overcome the concurrent requests with the same idempotency key issue, we defined the IIdempotencyCache interface and implemented the GetOrSet method. This is implemented in our build-in DistributedCache caching project, which is based on the standard IDistributedCache interface.

Our implementation provides basic caching functionality. However, by defining the IIdempotencyCache interface, our IdempotencyAPI logic becomes independent from the caching implementation. Thus, we can support other caching implementations with advanced features, such as the FusionCache.

FusionCache is a high-performance and robust caching .NET library with an optional distributed 2nd layer with advanced features, such as fail-safe mechanism, cache stampede prevention, fine-grained soft/hard timeouts with background factory completion, extensive customizable logging, and more.

BinaryFormatter is Obsolete

The BinaryFormatter serialization methods become obsolete from ASP .NET Core 5.0. In the IdempotentAPI project, the BinaryFormatter was used in the Utils.cs class for serialization and deserialization. As a result, our library was not working in .NET Core 5.0 and later versions unless we enabled the BinaryFormatterSerialization option in the .csproj file.

The recommended action based on the .NET documentation is to stop using BinaryFormatter and use a JSON or XML serializer. In our case, we used the Newtonsoft JsonSerializer, which can include type information when serializing JSON, and read this type of information when deserializing JSON to create the target object with the original types.

In the following JSON example, we can see how the type of information is included in the data.

{
  "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Object, System.Private.CoreLib]], System.Private.CoreLib",
  "Request.Method": "POST",
  "Response.StatusCode": 200,
  "Response.Headers": {
    "$type": "System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib],[System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib]], System.Private.CoreLib",
    "myHeader1": {
      "$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
      "$values": [
        "value1-1",
        "value1-2"
      ]
    },
    "myHeader2": {
      "$type": "System.Collections.Generic.List`1[[System.String, System.Private.CoreLib]], System.Private.CoreLib",
      "$values": [
        "value2-1",
        "value2-1"
      ]
    }
  }
}

Quick Start

Step 1: 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 (requiring registration) or 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.

IdempotentAPI.Cache
Implementation
Support Concurrent Requests Primary Cache 2nd-Level Cache Advanced Features
DistributedCache (Default) IDistributedCache
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();
// 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 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.Cache 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.

Summary

The IdempotentAPI 1.0.0-RC-01 is available with many improvements 🎉✨. With the community’s help, several issues and improvements have been identified and implemented. I want to take this opportunity to thank @apchenjun, @fjsosa, @lvzhuye, @RichardGreen-IS2, and @william-keller for your support, ideas, and time to improve this library.

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 😁.