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. For example, I am sure that you have heard of the microservices architecture, which is a kind of distributed system.
Creating Web APIs for distributed systems is challenging because of distribution pitfalls such as process failures, communication failures, asynchrony, and concurrency. One common requirement and challenge when building fault-tolerant distributed applications is the need to be idempotent.
- In mathematics and computer science, an operation is idempotent when applied multiple times without changing the result beyond the initial application.
- Fault-tolerant applications can continue operating despite the system, hardware, and network faults of one or more components, ensuring high availability and business continuity for critical applications or systems.
Idempotence in Web APIs ensures that the API works correctly (as designed) even when consumers (clients) send the same request multiple times. For example, this case can happen when the API failed to generate the response (due to process failures, temporary downtime, etc.) or because the response was generated but could not be transferred (network issues).
Imagine a scenario in which the user clicks a “Pay” button to make a purchase. For unknown reasons, the user receives an error, but the payment was completed. If the user clicks the “Pay” button again or the request is re-sent by a retry library, we would result in two payments! Using idempotency, the user will get a successful message (e.g., on the second try), but only one charge would be performed.
Creating Idempotent Web APIs is the first step before using a resilient and transient-fault-handling library, such as Polly. The Polly .NET library allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe manner.
The IdempotentAPI
library provides an easy way to develop idempotent Web APIs. In the following sections, we will see the idempotency in the different HTTP methods, how the IdempotentAPI
library works, its code, and finally, how to use the IdempotentAPI
NuGet packages.
HTTP defines a set of request methods (HTTP verbs: GET, POST, PUT, PATCH, etc.) to indicate the desired action to be performed for a given resource. An idempotent HTTP method can be called many times without resulting in different outcomes. Safe methods are HTTP methods that do not modify the resources. In Table 1, we can see details about which HTTP methods are idempotent or/and safe.
Table 1. - Idempotent or/and Safe HTTP methods (verbs).
HTTP Method | Idempotent | Safe | Description |
---|---|---|---|
GET | Yes | Yes | Safe HTTP methods do not modify resources. Thus, multiple calls with this method will always return the same response. |
OPTIONS | Yes | Yes | Same as the previous HTTP method. |
HEAD | Yes | Yes | Same as the previous HTTP method. |
PUT | Yes | No | The PUT HTTP method is idempotent because calling this HTTP method multiple times (with the same request data) will update the same resource and not change the outcome. |
DELETE | Yes | No | The DELETE HTTP method is idempotent because calling this HTTP method multiple times will only delete the resource once. Thus, numerous calls of the DELETE HTTP method will not change the outcome. |
POST | No | No | Calling the POST method multiple times can have different results and will create multiple resources. For that reason, the POST method is not idempotent. |
PATCH | No | No | The PATCH method can be idempotent depending on the implementation, but it isn’t required to be. For that reason, the PATCH method is not idempotent. |
The creation of an idempotent consumer
is an essential factor in HTTP idempotency. The API server would need a way to recognize subsequent retries of the same request. Commonly, the consumer generates a unique value, called idempotency-key, which the API server uses for that purpose. In addition, when building an idempotent consumer, it is recommended to:
- Use "V4 UUIDs" for the creation of the idempotency unique keys (e.g. “07cd2d27-e0dc-466f-8193-28453e9c3023”).
- Use techniques like the exponential backoff and random jitter, i.e., including an exponential and random delay between continuous requests.
The IdempotentAPI
is an open-source NuGet library, which implements an ASP.NET Core attribute (filter) to handle the HTTP write operations (POST and PATCH) that can affect only once for the given request data and idempotency-key.
The API consumer (e.g., a Front-End website) sends a request including an Idempotency-Key header unique identifier (default name: IdempotencyKey
). The API server checks if that unique identifier has been used previously for that request and either returns the cached response (without further execution) or save-cache the response along with the unique identifier. The cached response includes the HTTP status code, the response body, and headers.
Storing data is necessary for idempotency, but if the data are not expired after a certain period, it will include unneeded complexity in data storage, security, and scaling. Therefore, the data should have a retention period that makes sense for your problem domain.
The IdempotentAPI
library performs additional 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.
The following figure shows a simplified example of the IdempotentAPI
library flow for two exact POST requests. As shown, the IdempotentAPI
library includes two additional steps, one before the controller’s execution and one after constructing the controller’s response.
Figure 1. - A simplified example of the IdempotentAPI
flow for two exact POST requests.
- ⭐ Simple: Support idempotency in your APIs easily with 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 below for more details)
- Logging Level configuration
- ✳ NEW ✳ - Configure the Newtonsoft
SerializerSettings
based on your needs.
- 🔧 Caching Implementation based on your needs.
- 🏠
DistributedCache
: A build-in caching that is based on the standardIDistributedCache
interface. - 🦥 FusionCache: A high performance and robust cache with an optional distributed 2nd layer and some advanced features.
- ... or you could use your own implementation 😉
- 🏠
- 🔀 Support idempotency in a Cluster Environment (i.e., a group of multiple server instances) using Distributed Locks.
- samcook/RedLock.net: Supports the Redis Redlock algorithm.
- madelson/DistributedLock: Supports multiple technologies such as Redis, SqlServer, Postgres and many more.
- 💪Powerful: Can be used in high-load scenarios.
- 🌟 Supports Minimal APIs.
- ✳ NEW ✳ - 🌟 Support FastEndpoints, a developer-friendly alternative to Minimal APIs and MVC.
Package Name | Description | Release |
---|---|---|
IdempotentAPI | The implementation of the IdempotentAPI library. |
|
IdempotentAPI.AccessCache | The access cache implementation of the IdempotentAPI project. |
|
IdempotentAPI.MinimalAPI | The implementation to support IdempotentAPI in Minimal APIs. |
Package Name | Description | Release |
---|---|---|
IdempotentAPI.Cache.Abstractions | The cache definition of the IdempotentAPI project. | |
IdempotentAPI.Cache.DistributedCache | The default caching implementation, based on the standard IDistributedCache interface. |
|
IdempotentAPI.Cache.FusionCache | Supports caching via the FusionCache third-party library. |
Package Name | Description | Release |
---|---|---|
IdempotentAPI.DistributedAccessLock.Abstractions | The distributed access lock definition of the IdempotentAPI project. | |
IdempotentAPI.DistributedAccessLock.MadelsonDistributedLock | The Madelson DistributedLock implementation for the definition of the IdempotentAPI.DistributedAccessLock. | |
IdempotentAPI.DistributedAccessLock.RedLockNet | The RedLockNet implementation for the definition of the IdempotentAPI.DistributedAccessLock. |
Let's see how we could use the NuGet packages in a Web API project. For more examples and code, you can check the sample projects. The IdempotentAPI
can be installed via the NuGet UI or the NuGet package manager console:
PM> Install-Package IdempotentAPI -Version 2.6.0
and, register the IdempotentAPI Core services for either controller-based APIs or minimal APIs.
//------------------------------
// For Controller-Based APIs
//------------------------------
// Option 1: Use the options defined per Controller (i.e., in the attributes).
services.AddIdempotentAPI();
// Option 2: Register the `IIdempotencyOptions` that will enable the use of the `[Idempotent(UseIdempotencyOption = true)]` option to set global default options.
services.AddIdempotentAPI(idempotencyOptions);
OR
//------------------------------
// For Minimal APIs:
//------------------------------
// Option 1: Define the default options:
builder.Services.AddIdempotentMinimalAPI(new IdempotencyOptions());
// Option 2: Configure the idempotent options by implementing the `IIdempotencyOptionsProvider` to provide the `IIdempotencyOptions` based on our needs (e.g., per endpoint). For additional information, you can read the CHANGELOG.md.
builder.Services.AddIdempotentMinimalAPI(new IdempotencyOptionsProvider());
As we have seen, 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) |
✔️ |
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();
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 example:
// 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();
Currently, we support the following two implementations to support idempotency in a Cluster Environment (i.e., a group of multiple server instances) using Distributed Locks.
The DistributedLockTimeoutMilli
attribute option should be used to set the time the distributed lock will wait for the lock to be acquired (in milliseconds).
Supported Technologies | |
---|---|
samcook/RedLock.net | Redis Redlock |
madelson/DistributedLock | Redis, SqlServer, Postgres and many more. |
If you do not need to support idempotency in a Cluster Environment, you do not have to register anything. So, skip this step 😉.
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);
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();
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; }
}
}
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 on each action separately. The following two sections describe these two cases. However, we should define the Consumes
and Produces
attributes on the controller in both cases.
using IdempotentAPI.Filters;
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
{
// ...
}
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)
{
// ...
}
The latest IdempotentAPI.MinimalAPI package should be installed and then add the IdempotentAPIEndpointFilter
in your endpoints.
app.MapPost("/example",
([FromQuery] string yourParam) =>
{
return Results.Ok(new ResponseDTOs());
})
.AddEndpointFilter<IdempotentAPIEndpointFilter>();
The Idempotent attribute provides a list of options, as shown in the following table.
Table 2. - Idempotent attribute options
Name | Type | Default Value | Description |
---|---|---|---|
Enabled | bool | true | Enable or Disable the Idempotent operation on an API Controller’s class or method. |
ExpireHours (Obsolete) | int | 24 hours | The retention period (in hours) of the idempotent cached data. This option will be deprecated. |
ExpiresInMilliseconds | double | 24 hours | The retention period (in milliseconds) of the idempotent cached data. |
HeaderKeyName | string | IdempotencyKey | The name of the Idempotency-Key header. |
DistributedCacheKeysPrefix | string | IdempAPI_ | A prefix for the DistributedCache key names. |
CacheOnlySuccessResponses | bool | True | When true, only the responses with 2xx HTTP status codes will be cached. |
DistributedLockTimeoutMilli | double | NULL | The time the distributed lock will wait for the lock to be acquired (in milliseconds). This is Required when a IDistributedAccessLockProvider is provided. |
IsIdempotencyOptional | bool | False | Set the idempotency as optional to be introduced to existing endpoints easily (which should be backward compatible). |
Let’s have a quick look at the IdempotentAPI
project and its main code files.
- /Core/Idempotency.cs: The core implementation containing the idempotency logic applied before and after the request’s execution.
- /Filters/IdempotencyAttributeFilter.cs: The filter implementation (of
IActionFilter
andIResultFilter
) uses the core implementation on specific steps of the filter pipeline execution flow. - /Filters/IdempotencyAttribute.cs: The idempotency attribute implementation for its input options and to initialize the idempotency filter.
- /Helpers/Utils.cs: Several helper static functions for serialization, hashing, and compression.
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. As a result, 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. Applying idempotency in our APIs is the first step before using a resilient and transient-fault-handling library, such as Polly.
The IdempotentAPI
is an open-source NuGet library, which implements an ASP.NET Core attribute (filter) to handle the HTTP write operations (POST and PATCH) that can affect only once for the given request data and idempotency-key.
To ensure high availability and business continuity of your critical Web APIs, the IdempotentAPI
library is your first step 😉. Any help in coding, suggestions, giving a GitHub Star ⭐, etc., are welcome.
The IdempotentAPI
is MIT licensed.