Idempotency
Making an endpoint idempotent simply means that it will return the exact same response (from a cache) for a particular "unique" request everytime until the cached response is purged from the cache. The first time a particular request comes in, the cache will be checked to see if that exact request has come in before. If the request is not an original request and there is a cached response for that exact request, the previously cached response is served without executing the endpoint handler for it. If it is an original request and there is no cached response, the endpoint handler is executed, the response is cached and returned to the client. Any subsequent/repeated requests consisting of the exact same set of parameters such as headers, route/query params, request body, will result in the cached response being served without executing the endpoint.
Server-Side Setup
To make endpoints idempotent, simply enable the idempotency middleware and configure idempotency for the endpoints like so:
var bld = WebApplication.CreateBuilder();
bld.Services
.AddFastEndpoints()
.AddIdempotency(); //add this
var app = bld.Build();
app.UseOutputCache() //add this before FE
.UseFastEndpoints();
app.Run();
sealed class MyEndpoint : EndpointWithoutRequest
{
public override void Configure()
{
Get("my-endpoint");
AllowAnonymous();
Idempotency(); //add this
}
public override async Task HandleAsync(CancellationToken c)
{
await SendAsync($"TimeStamp: {DateTime.Now.Ticks}");
}
}
Client-Side Setup
The only special requirement for the client is to simply send in a header with the name Idempotency-Key (customizable), and a unique value per each request. There is no particular format requirement from the server, as long as the value is different per each unique request (user action). An example Curl request would look like this:
curl -X 'GET' \
'http://localhost:5000/my-endpoint' \
-H 'accept: text/plain' \
-H 'idempotency-key: 1dc3d9a8527047069f8056175a71fe79'
Requests to idempotent endpoints cannot be made without the Idempotency-Key header. A 400 - Bad Request will be served if the header is missing from the request.
Request Uniqueness
The uniqueness of incoming requests to an endpoint is determined by generating a Cache-Key. By default, the following request parameters participate in the cache-key generation:
- URL including scheme, host, port, path
- Route parameters
- Query parameters
- Idempotency-Key header
- A limited set of additional headers (customizable)
- Form data including form fields, form file names & file sizes (actual file bytes are not considered)
- Body content (be it JSON or anything else)
Multiple requests containing the exact same set of request parameter values will be identified as duplicates and only the first request will result in the endpoint handler being executed. The subsequent duplicate requests will receive the cached response generated by the initial request.
Performance Considerations
Idempotency should not be enabled for all endpoints of a system. It should be carefully chosen only for endpoints that actually require idempotency where there is a potential for trouble from duplicate requests for the same client/user action. Such as in payment processing where duplicate requests would result in duplicate charges to a customer.
The performance impact lies in the fact that each incoming request must be inspected by the middleware in order to generate the cache-key. The inspection of the request body stream causes the data to be buffered to memory/disk. Due to this reason, idempotent endpoints will never be as performant as regular endpoints. Performance is the price you pay for this safety feature.
If the requesting clients are under strict quality control and can be guaranteed that the same idempotency-key value will not be mistakenly re-used, you can mitigate most of this performance impact by making the request body content not participate in the cache-key generation. In which case, the uniqueness of the requests will be determined solely by the request URL, route/query params and header values. The body content will be ignored, and no content buffering will occur.
Customization Options
The following options can be customized per endpoint as well as globally with the use of an endpoint configurator.
Idempotency(
o =>
{
//idempotency-key header name.
o.HeaderName = "Idempotency-Key";
//additional header names to be considered for cache-key generation.
o.AdditionalHeaders.Add("My-Header");
//controls whether body content participates in cache-key generation.
o.IgnoreRequestBody = true;
//the time limit to cache responses for.
o.CacheDuration = TimeSpan.FromDays(1);
//automatically adds the idempotency-key header to the response.
o.AddHeaderToResponse = true;
//text description for the swagger request header parameter.
o.SwaggerHeaderDescription = "This is an idempotent endpoint.";
//a function to generate example values for the swagger parameter.
o.SwaggerExampleGenerator = () => Guid.NewGuid().ToString("N");
//used to determine the format of the swagger parameter.
o.SwaggerHeaderType = typeof(Guid);
});
Distributed Cache Storage
The idempotency feature is implemented as a custom cache policy for the built-in output caching middleware (only available in .NET 7+). By default, the output caching middleware uses the in-memory cache storage provider. You can either plug in your own implementation of IOutputCacheStore or use the Microsoft provided Redis provider.