Explores support for idempotency
This PR adds opt-in support for idempotent requests in JsonApiDotNetCore, as defined by IETF.
In a microservices world, where at-least-once delivery is common practice, APIs must be able to handle duplicate requests gracefully. According to the HTTP standard, only POST and PATCH are non-idempotent by nature. Since in JSON:API there is no way to express "multiply the existing value of attribute X by 3", we only need to handle POST.
When a client includes the "Idempotency-Key" HTTP header in a POST Resource (or atomic:operations) request, the server stores the response before sending it back. A subsequent POST Resource request with the same key just returns the recorded response instead of executing the request. In such cases, it sends back the idempotency key too, to inform the client this was a replay.
The mechanism applies to both success and error responses. If the client wants to retry a failed request, it should resend with another key.
The tricky part is handling concurrent requests with the same key. The chosen approach here is to wrap the entire request in a transaction, so that concurrent requests block on trying to insert a record with the same primary key. This varies per database provider and isolation level, so needs more testing. An even more scalable solution would be to use an expiring lease that is atomically obtained, but then we'd need to know the maximum duration that processing a request can take. This is problematic when non-idempotent downstream external network service calls are made from resource definitions.
References on idempotency:
- https://tools.ietf.org/id/draft-idempotency-header-01.html
- https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb
- https://stripe.com/docs/idempotency / https://stripe.com/docs/api/idempotent_requests
- https://brandur.org/idempotency-keys
References on capturing request/response bodies in ASP.NET:
- https://elanderson.net/2019/12/log-requests-and-responses-in-asp-net-core-3/
- https://stackoverflow.com/questions/43403941/how-to-read-asp-net-core-response-body
- https://exceptionnotfound.net/using-middleware-to-log-requests-and-responses-in-asp-net-core/
- https://github.com/Treit/LoggingMiddlewareExample
Codecov Report
Merging #1132 (b98933e) into master (ea3ab72) will increase coverage by
0.03%. The diff coverage is95.94%.
:exclamation: Current head b98933e differs from pull request most recent head 08f8141. Consider uploading reports for the commit 08f8141 to get more accurate results
@@ Coverage Diff @@
## master #1132 +/- ##
==========================================
+ Coverage 92.61% 92.64% +0.03%
==========================================
Files 242 244 +2
Lines 7701 7847 +146
==========================================
+ Hits 7132 7270 +138
- Misses 569 577 +8
| Impacted Files | Coverage Δ | |
|---|---|---|
| ...nApiDotNetCore/Middleware/NoIdempotencyProvider.cs | 25.00% <25.00%> (ø) |
|
| ...nApiDotNetCore/Middleware/IdempotencyMiddleware.cs | 97.45% <97.45%> (ø) |
|
| ...AtomicOperations/EntityFrameworkCoreTransaction.cs | 100.00% <100.00%> (ø) |
|
| ...perations/EntityFrameworkCoreTransactionFactory.cs | 100.00% <100.00%> (ø) |
|
| ...Core/Configuration/ApplicationBuilderExtensions.cs | 100.00% <100.00%> (ø) |
|
| ...NetCore/Configuration/JsonApiApplicationBuilder.cs | 100.00% <100.00%> (ø) |
|
| ...JsonApiDotNetCore/Middleware/IdempotentResponse.cs | 100.00% <100.00%> (ø) |
|
| src/JsonApiDotNetCore.Annotations/ArgumentGuard.cs | 73.33% <0.00%> (-6.67%) |
:arrow_down: |
| ...ApiDotNetCore.SourceGenerators/SourceCodeWriter.cs | 97.47% <0.00%> (-0.09%) |
:arrow_down: |
| ...Core/Repositories/EntityFrameworkCoreRepository.cs | 99.29% <0.00%> (-0.03%) |
:arrow_down: |
| ... and 15 more |
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.