{"id":1254,"date":"2025-11-06T10:00:40","date_gmt":"2025-11-06T10:00:40","guid":{"rendered":"https:\/\/oussamasaidi.com\/?p=1254"},"modified":"2025-12-20T11:14:43","modified_gmt":"2025-12-20T11:14:43","slug":"how-to-build-an-idempotent-api-with-net-9-ef-core-9-and-polly-v8","status":"publish","type":"post","link":"https:\/\/oussamasaidi.com\/en\/how-to-build-an-idempotent-api-with-net-9-ef-core-9-and-polly-v8\/","title":{"rendered":"How to Build an Idempotent API with .NET 9, EF Core 9 and Polly v8"},"content":{"rendered":"<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>Have you ever hit <em>\u201cPay\u201d<\/em> on a checkout page, the page froze, and you clicked again \u2014 only to be charged twice?<br>That\u2019s exactly what <strong>idempotency<\/strong> prevents.<\/p>\n\n\n\n<p>In distributed and cloud-native architectures, <strong>duplicate requests<\/strong> happen often:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>client-side retries (mobile poor signal),<\/li>\n\n\n\n<li>API Gateway or load-balancer retries,<\/li>\n\n\n\n<li>users refreshing a browser form.<\/li>\n<\/ul>\n\n\n\n<p>An <strong>idempotent API<\/strong> guarantees that <strong>executing the same request multiple times produces the same result<\/strong> \u2014 no side effects, no duplicates, no surprises.<\/p>\n\n\n\n<p>In this detailed tutorial, we\u2019ll build such an API using:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>.NET 9 Minimal APIs<\/strong> (lightweight and fast)<\/li>\n\n\n\n<li><strong>Entity Framework Core 9<\/strong> (to persist requests and responses)<\/li>\n\n\n\n<li><strong>Polly v8<\/strong> (for safe retry handling)<\/li>\n\n\n\n<li><strong>SQLite + MemoryCache<\/strong> (for persistence and replay)<\/li>\n\n\n\n<li><strong>Docker<\/strong> (for containerized deployment)<\/li>\n<\/ul>\n\n\n\n<p>All source code is available on <a href=\"http:\/\/github.com\/oussama-saidi\/os-tuto-net-core-rest-api-Idempotent\">GitHub <\/a><br> <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What Is Idempotency?<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Definition:<\/strong><br>An operation is <em>idempotent<\/em> if performing it multiple times has the <strong>same effect<\/strong> as doing it once.<\/p>\n<\/blockquote>\n\n\n\n<p>Example:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>GET \/users\/5<\/code> \u2192 safe to call 100 times<\/li>\n\n\n\n<li><code>PUT \/users\/5<\/code> with same data \u2192 always same outcome<\/li>\n\n\n\n<li><code>DELETE \/users\/5<\/code> \u2192 deleting an already-deleted resource is harmless<\/li>\n\n\n\n<li><code>POST \/payments<\/code> \u2192 &#x26a0;&#xfe0f; usually creates duplicates if repeated<\/li>\n<\/ul>\n\n\n\n<p>So <code>POST<\/code> operations must implement <strong>custom logic<\/strong> to become idempotent.<br>This is especially important for <strong>financial<\/strong>, <strong>order<\/strong>, and <strong>API-to-API<\/strong> systems.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Solution Overview<\/h2>\n\n\n\n<p>Our approach adds a lightweight <strong>Idempotency Layer<\/strong> on top of your API.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"> Architecture Summary<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The <strong>client<\/strong> includes a unique <code>Idempotency-Key<\/code> in the request header.<\/li>\n\n\n\n<li>The API calculates a <strong>hash<\/strong> of the request body (payload fingerprint).<\/li>\n\n\n\n<li>The API checks if this key exists in storage (DB or cache).<\/li>\n\n\n\n<li>If found, it returns the <strong>same stored response<\/strong> instead of re-executing logic.<\/li>\n\n\n\n<li>If not found, it processes normally, saves the response, and returns it.<\/li>\n<\/ol>\n\n\n\n<p>This pattern ensures the request can be <strong>safely retried<\/strong> without side effects.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Project Architecture<\/h2>\n\n\n\n<p>Our solution consists of:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Minimal API (.NET 9)<\/strong> \u2192 lightweight entry point<\/li>\n\n\n\n<li><strong>EF Core 9 + SQLite<\/strong> \u2192 persistent storage for payments &amp; keys<\/li>\n\n\n\n<li><strong>Idempotency Store<\/strong> \u2192 saves the request hash + response<\/li>\n\n\n\n<li><strong>Memory Cache<\/strong> \u2192 fast replay for repeated keys<\/li>\n\n\n\n<li><strong>Polly v8<\/strong> \u2192 retry policy for resilient HTTP clients<\/li>\n\n\n\n<li><strong>Docker &amp; GitHub Actions<\/strong> \u2192 deployment + CI<\/li>\n<\/ul>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"postgresql\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">OS.Tuto.IdempotentApi\/\n \u251c\u2500 Program.cs\n \u251c\u2500 Data\/\n \u2502   \u2514\u2500 AppDbContext.cs\n \u251c\u2500 Domain\/\n \u2502   \u251c\u2500 Payment.cs\n \u2502   \u2514\u2500 PaymentStatus.cs\n \u251c\u2500 Dtos\/\n \u2502   \u251c\u2500 PaymentRequest.cs\n \u2502   \u2514\u2500 PaymentResponse.cs\n \u251c\u2500 Idempotency\/\n \u2502   \u251c\u2500 IdempotencyRecord.cs\n \u2502   \u2514\u2500 IdempotencyStore.cs\n \u251c\u2500 Client\/\n \u2502   \u2514\u2500 Program.cs\n \u251c\u2500 Dockerfile\n \u251c\u2500 docker-compose.yml\n \u251c\u2500 appsettings.json\n \u2514\u2500 README.md\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 1 \u2014 Project Setup (.NET 9 Minimal API)<\/h2>\n\n\n\n<p>In your terminal:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet new web -n OS.Tuto.IdempotentApi\ncd OS.Tuto.IdempotentApi\ndotnet add package Microsoft.EntityFrameworkCore\ndotnet add package Microsoft.EntityFrameworkCore.Sqlite\ndotnet add package Microsoft.Extensions.Http.Resilience\n<\/pre>\n\n\n\n<p>This initializes a clean .NET 9 Minimal API project.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 2 \u2014 Add Database Context and Entities<\/h2>\n\n\n\n<p>We\u2019ll persist payments and idempotent requests in SQLite.<\/p>\n\n\n\n<p>Example: <code>AppDbContext<\/code><\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public class AppDbContext : DbContext\n{\n    public DbSet&lt;Payment> Payments => Set&lt;Payment>();\n    public DbSet&lt;IdempotencyRecord> IdempotencyRecords => Set&lt;IdempotencyRecord>();\n    public AppDbContext(DbContextOptions&lt;AppDbContext> options) : base(options) { }\n}\n<\/pre>\n\n\n\n<p>Payment Entity (simplified)<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public class Payment\n{\n    public Guid Id { get; set; } = Guid.NewGuid();\n    public decimal Amount { get; set; }\n    public string Currency { get; set; } = \"EUR\";\n    public string Recipient { get; set; } = default!;\n    public string IdempotencyKey { get; set; } = default!;\n    public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;\n}\n<\/pre>\n\n\n\n<p>Each payment stores the key that triggered it \u2014 ensuring the same key cannot produce another row.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 3 \u2014 Implement the Idempotency Store<\/h2>\n\n\n\n<p>The store is the heart of the system.<br>It remembers which keys were processed and what the API returned.<\/p>\n\n\n\n<p>Simplified Logic<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">public class IdempotencyStore\n{\n    private readonly AppDbContext _db;\n    private readonly IMemoryCache _cache;\n    public IdempotencyStore(AppDbContext db, IMemoryCache cache)\n    { _db = db; _cache = cache; }\n\n    public async Task&lt;(bool found, string response)> TryGetAsync(string key)\n    {\n        if (_cache.TryGetValue(key, out string body))\n            return (true, body);\n\n        var rec = await _db.IdempotencyRecords.FirstOrDefaultAsync(x => x.Key == key);\n        return rec is null ? (false, \"\") : (true, rec.ResponseBody);\n    }\n\n    public async Task SaveAsync(string key, object response)\n    {\n        var json = JsonSerializer.Serialize(response);\n        _db.IdempotencyRecords.Add(new() { Key = key, ResponseBody = json });\n        await _db.SaveChangesAsync();\n        _cache.Set(key, json);\n    }\n}\n<\/pre>\n\n\n\n<p>Each processed request is cached in-memory and persisted in the database for replay.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 4 \u2014 Make the POST Endpoint Idempotent<\/h2>\n\n\n\n<p>Let\u2019s modify our <code>\/payments<\/code> endpoint to use <code>Idempotency-Key<\/code>.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">app.MapPost(\"\/payments\", async (PaymentRequest req, HttpContext ctx, AppDbContext db, IdempotencyStore store) =>\n{\n    var key = ctx.Request.Headers[\"Idempotency-Key\"].FirstOrDefault();\n    if (string.IsNullOrEmpty(key)) return Results.BadRequest(\"Missing Idempotency-Key\");\n\n    var (found, body) = await store.TryGetAsync(key);\n    if (found) return Results.Content(body, \"application\/json\");\n\n    var payment = new Payment { Amount = req.Amount, Currency = req.Currency, Recipient = req.Recipient, IdempotencyKey = key };\n    db.Payments.Add(payment);\n    await db.SaveChangesAsync();\n\n    var response = new { payment.Id, payment.Amount, payment.Recipient };\n    await store.SaveAsync(key, response);\n\n    return Results.Ok(response);\n});\n<\/pre>\n\n\n\n<p>&#x2705; <strong>First call:<\/strong> creates and stores payment.<br>&#x2705; <strong>Repeated call with same key:<\/strong> returns same JSON response instantly.<br>&#x26a0;&#xfe0f; <strong>Same key + different body:<\/strong> can be handled as conflict (optional check).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 5 \u2014 Testing with Curl or Postman<\/h2>\n\n\n\n<p>Create a payment<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">curl -s http:\/\/localhost:5000\/payments \\\n  -H \"Content-Type: application\/json\" \\\n  -H \"Idempotency-Key: key-123\" \\\n  -d '{\"amount\": 99.99, \"currency\": \"EUR\", \"recipient\": \"alice@example.com\"}'\n<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Re-run same request<\/h3>\n\n\n\n<p>\u2192 Returns <strong>same response<\/strong> \u2014 no duplicate row.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Change payload, keep same key<\/h3>\n\n\n\n<p>\u2192 Returns <code>409 Conflict<\/code> (optional validation step).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f; Step 6 \u2014 Safe Retry Using Polly v8<\/h2>\n\n\n\n<p>When clients or background workers call your API, transient failures can happen.<br>By combining <strong>Polly<\/strong> with our idempotent endpoint, retries become <strong>100% safe<\/strong>.<\/p>\n\n\n\n<p>Simple retry setup<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">builder.Services.AddHttpClient(\"PaymentsClient\")\n    .AddResilienceHandler(\"retry\", rb =>\n        rb.AddRetry(new() { MaxRetryAttempts = 3, Delay = TimeSpan.FromSeconds(2) }));\n<\/pre>\n\n\n\n<p>Each retry uses the same <code>Idempotency-Key<\/code>.<br>Even if the client loses connection mid-response, the next call retrieves the same result.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f;Step 7 \u2014 Dockerize the API<\/h2>\n\n\n\n<p>Create a <code>Dockerfile<\/code> for container deployme<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM mcr.microsoft.com\/dotnet\/sdk:9.0 AS build\nWORKDIR \/src\nCOPY . .\nRUN dotnet publish -c Release -o \/app\nFROM mcr.microsoft.com\/dotnet\/aspnet:9.0\nWORKDIR \/app\nCOPY --from=build \/app .\nEXPOSE 8080\nENTRYPOINT [\"dotnet\", \"OS.Tuto.IdempotentApi.dll\"]\n<\/pre>\n\n\n\n<p>Then run:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">docker build -t idempotent-api .\ndocker run -p 8080:8080 idempotent-api\n<\/pre>\n\n\n\n<p>Access it on <strong><a>http:\/\/localhost:8080\/payments<\/a><\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">&#x2699;&#xfe0f;Step 8 \u2014 Continuous Integration (GitHub Actions)<\/h2>\n\n\n\n<p>GitHub Actions ensure the project builds and tests automatically.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">name: dotnet-build\non: [push, pull_request]\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v4\n      - uses: actions\/setup-dotnet@v4\n        with:\n          dotnet-version: 9.0.x\n      - run: dotnet restore\n      - run: dotnet build -c Release\n<\/pre>\n\n\n\n<p>Now each commit validates your solution.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Approach Works<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Feature<\/th><th>Description<\/th><\/tr><\/thead><tbody><tr><td><strong>Deterministic behavior<\/strong><\/td><td>Same request = same result<\/td><\/tr><tr><td><strong>Crash recovery<\/strong><\/td><td>If app restarts mid-request, replayed call returns saved response<\/td><\/tr><tr><td><strong>No double billing<\/strong><\/td><td>Perfect for payments and transactions<\/td><\/tr><tr><td><strong>Safe retry<\/strong><\/td><td>Integrates with Polly or API Gateway retries<\/td><\/tr><tr><td><strong>Scalable<\/strong><\/td><td>Replace IMemoryCache with Redis for distributed caching<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Going Distributed (Redis Option)<\/h2>\n\n\n\n<p>In production, multiple API instances may run behind a load balancer.<br>To make idempotency global, switch caching to <strong>Redis<\/strong>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"bash\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis\n<\/pre>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">builder.Services.AddStackExchangeRedisCache(o =>\n    o.Configuration = \"localhost:6379\");\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Common Pitfalls and Best Practices<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Pitfall<\/th><th>Fix<\/th><\/tr><\/thead><tbody><tr><td>Reusing the same key with different payload<\/td><td>Compare request hashes; reject with 409<\/td><\/tr><tr><td>Very long TTL<\/td><td>Keep key records short (e.g., 24 h max)<\/td><\/tr><tr><td>Missing key header<\/td><td>Return <code>400 Bad Request<\/code> early<\/td><\/tr><tr><td>Non-deterministic logic<\/td><td>Avoid timestamps\/random outputs when replaying<\/td><\/tr><tr><td>Memory cache only<\/td><td>Use distributed cache in multi-instance setups<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">When to Use Idempotency<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Use Case<\/th><th>Why It Matters<\/th><\/tr><\/thead><tbody><tr><td><strong>Payment APIs<\/strong><\/td><td>Prevent double charges<\/td><\/tr><tr><td><strong>Order Checkout<\/strong><\/td><td>Avoid duplicate orders<\/td><\/tr><tr><td><strong>Webhook Consumers<\/strong><\/td><td>External systems often resend events<\/td><\/tr><tr><td><strong>IoT Devices<\/strong><\/td><td>Network drops cause resend<\/td><\/tr><tr><td><strong>Background jobs<\/strong><\/td><td>Retry logic needs determinism<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Simplified Flow Diagram<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"visualbasic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">Client \u2500\u2500\u25ba API with Idempotency-Key\n           \u2502\n           \u251c\u2500 Key not found \u2192 Execute business logic\n           \u2502                  Store response in DB + cache\n           \u2514\u2500 Key found     \u2192 Return stored response\n<\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"1024\" src=\"https:\/\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/oussama-saidi-tuto-net-core-rest-api-Idempotent-2-1.png\" alt=\"\" class=\"wp-image-1264\" srcset=\"https:\/\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/oussama-saidi-tuto-net-core-rest-api-Idempotent-2-1.png 1024w, https:\/\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/oussama-saidi-tuto-net-core-rest-api-Idempotent-2-1-300x300.png 300w, https:\/\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/oussama-saidi-tuto-net-core-rest-api-Idempotent-2-1-150x150.png 150w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Every retry simply reads the stored result \u2014 simple, predictable, safe.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\"> Performance Notes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>First execution = database + compute<\/li>\n\n\n\n<li>Next executions = cache only \u2192 &#x26a1; fast<\/li>\n\n\n\n<li>MemoryCache TTL = 5 min (default)<\/li>\n\n\n\n<li>Redis or SQL backend recommended for multi-region deployments<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Technologies Used<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Component<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><strong>.NET 9<\/strong><\/td><td>Minimal API framework<\/td><\/tr><tr><td><strong>EF Core 9<\/strong><\/td><td>ORM + migrations<\/td><\/tr><tr><td><strong>SQLite<\/strong><\/td><td>Lightweight data store<\/td><\/tr><tr><td><strong>Polly v8<\/strong><\/td><td>Retry &amp; resilience<\/td><\/tr><tr><td><strong>Docker<\/strong><\/td><td>Packaging &amp; deployment<\/td><\/tr><tr><td><strong>Redis (optional)<\/strong><\/td><td>Distributed cache<\/td><\/tr><tr><td><strong>GitHub Actions<\/strong><\/td><td>Continuous integration<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Full Source Code<\/h2>\n\n\n\n<p>All source code, configuration files, and Docker setup are publicly available on GitHub <\/p>\n\n\n\n<p> <strong>Repository:<\/strong> <a href=\"https:\/\/github.com\/oussama-saidi\/os-tuto-net-core-rest-api-Idempotent\">https:\/\/github.com\/oussama-saidi\/os-tuto-net-core-rest-api-Idempotent<\/a><\/p>\n\n\n\n<p>The repo includes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Complete .NET 9 solution<\/li>\n\n\n\n<li>EF Core migrations<\/li>\n\n\n\n<li>Dockerfile + docker-compose<\/li>\n\n\n\n<li>CI\/CD workflow<\/li>\n\n\n\n<li>Example client with Polly retry logic<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Key Takeaways<\/h2>\n\n\n\n<p>&#x2705; Idempotency = <em>safe retries<\/em> and <em>predictable APIs<\/em><br>&#x2705; Protects against <strong>duplicate operations<\/strong><br>&#x2705; Easy to implement using <strong>.NET 9<\/strong> and <strong>EF Core 9<\/strong><br>&#x2705; Supports <strong>resilience<\/strong> with <strong>Polly v8<\/strong><br>&#x2705; Ready for <strong>Docker &amp; GitHub Actions<\/strong> deployment<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Idempotency isn\u2019t just a nice-to-have \u2014 it\u2019s a core principle of <strong>modern API design<\/strong>.<br>Whether you\u2019re handling payments, notifications, or background tasks, implementing idempotency ensures your system behaves consistently, even under retries or network issues.<\/p>\n\n\n\n<p>With <strong>.NET 9<\/strong>, <strong>EF Core 9<\/strong>, and <strong>Polly v8<\/strong>, achieving this pattern is simple, elegant, and production-ready.<\/p>\n\n\n\n<p>You can explore and clone the full working example from GitHub:<br> <a href=\"https:\/\/github.com\/oussama-saidi\/os-tuto-net-core-rest-api-Idempotent\">https:\/\/github.com\/oussama-saidi\/os-tuto-net-core-rest-api-Idempotent<\/a><\/p>\n\n\n\n<div class=\"buy-coffee-container\">\n<p style=\"text-align:center; color:#555; font-size:14px;\">\n  If this article helped you, consider supporting my work.\n<\/p>\n  <a\n    href=\"https:\/\/buymeacoffee.com\/oussamasaii\"\n    target=\"_blank\"\n    rel=\"noopener noreferrer\"\n    class=\"buy-coffee-button\"\n  >\n    &#x2615; Buy me a coffee\n  <\/a>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>Introduction Have you ever hit \u201cPay\u201d on a checkout page, the page froze, and you clicked again \u2014 only to&#8230; <\/p>\n<div class=\"art-el-more\"><a href=\"https:\/\/oussamasaidi.com\/en\/how-to-build-an-idempotent-api-with-net-9-ef-core-9-and-polly-v8\/\" class=\"art-link art-color-link art-w-chevron\">Read more<\/a><\/div>","protected":false},"author":1,"featured_media":1265,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[40,68,45,41,56,17,85,84,82,83,98,20,61],"tags":[47,48,49,54,97,53],"ppma_author":[286],"class_list":["post-1254","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-netcore","category-api","category-asp-net","category-c","category-c-2","category-design","category-docker","category-ef-core","category-entity-framework-core","category-entity-framework-core-2","category-solid","category-technology","category-web-api","tag-net-core","tag-asp-net","tag-c-sharp","tag-c","tag-clean-code","tag-dot-net-core"],"acf":[],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/oussama-saidi-tuto-net-core-rest-api-Idempotent-1.png","jetpack_sharing_enabled":true,"jetpack-related-posts":[{"id":1714,"url":"https:\/\/oussamasaidi.com\/en\/https-oussamasaidi-com-restful-api-mastery-best-practices-with-asp-net-core-part-2\/","url_meta":{"origin":1254,"position":0},"title":"RESTful API best practices\u00a0with ASP.NET Core Part 2","author":"Saidi Oussama","date":"December 20, 2025","format":false,"excerpt":"Testing, Performance, Security, Microservices & Deployment Introduction: From Solid Foundations to Production Excellence In Part 1 of RESTful API Mastery, we established the architectural and technical foundations required to build reliable, evolvable RESTful APIs with ASP.NET Core. However, a well-designed API only becomes truly valuable when it is tested, observable,\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"RESTful API Mastery","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-2r.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":1639,"url":"https:\/\/oussamasaidi.com\/en\/restful-api-mastery-best-practices-with-asp-net-core\/","url_meta":{"origin":1254,"position":1},"title":"RESTful API Best Practices with ASP.NET Core","author":"Saidi Oussama","date":"December 16, 2025","format":false,"excerpt":"Professional Best Practices, Versioning Strategies & Advanced Serialization (Part 1) In this blog Introduction: Building Enterprise-Grade RESTful APIs with ASP.NET Core1. RESTful APIs in the Modern ASP.NET Core EcosystemWhy REST Still Dominates2. REST Architectural Constraints Every ASP.NET Core API Must EnforceClient\u2013Server SeparationStatelessnessUniform Interface3. Establishing a Clean and Scalable ASP.NET Core\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"RESTful API Mastery","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/12\/restful-api-mastery-best-practices-with-asp-net-core-cover-scaled.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":1325,"url":"https:\/\/oussamasaidi.com\/en\/building-professional-modern-api-documentation-in-net-core-with-scalar\/","url_meta":{"origin":1254,"position":2},"title":"Building Professional, Modern API Documentation in .NET Core with Scalar","author":"Saidi Oussama","date":"November 19, 2025","format":false,"excerpt":"Introduction In today\u2019s software ecosystem, APIs are everywhere. Whether you are building a mobile application, a microservices architecture, or an internal company platform, your API is often the backbone of the system. But even the best API becomes useless if developers cannot understand how to consume it. This is why\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"Building Professional, Modern API Documentation in .NET Core with Scalar","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/Building-Professional-Modern-API-Documentation-in-.NET-Core-with-Scalar.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":931,"url":"https:\/\/oussamasaidi.com\/en\/net-9-entity-framework-core-what-is-new\/","url_meta":{"origin":1254,"position":3},"title":"Les Nouveaut\u00e9s d&rsquo;Entity Framework Core avec .NET 9 : Ce Qu&rsquo;il Faut Savoir","author":"Saidi Oussama","date":"January 6, 2025","format":false,"excerpt":"La sortie de .NET 9 s\u2019accompagne de mises \u00e0 jour significatives dans Entity Framework Core (EF Core), le c\u00e9l\u00e8bre ORM de Microsoft. Ces nouveaut\u00e9s visent \u00e0 simplifier les sc\u00e9narios complexes et \u00e0 am\u00e9liorer les performances, la compatibilit\u00e9 cloud et l\u2019int\u00e9gration IA. Voici les cinq principales nouveaut\u00e9s qui transformeront vos projets.\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/01\/DALL%C2%B7E-2025-01-06-15.03.46-A-professional-and-modern-banner-image-for-a-WordPress-blog-article-about-the-new-features-of-Entity-Framework-Core-with-.NET-9.-The-design-should-inc.webp?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/01\/DALL%C2%B7E-2025-01-06-15.03.46-A-professional-and-modern-banner-image-for-a-WordPress-blog-article-about-the-new-features-of-Entity-Framework-Core-with-.NET-9.-The-design-should-inc.webp?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/01\/DALL%C2%B7E-2025-01-06-15.03.46-A-professional-and-modern-banner-image-for-a-WordPress-blog-article-about-the-new-features-of-Entity-Framework-Core-with-.NET-9.-The-design-should-inc.webp?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/01\/DALL%C2%B7E-2025-01-06-15.03.46-A-professional-and-modern-banner-image-for-a-WordPress-blog-article-about-the-new-features-of-Entity-Framework-Core-with-.NET-9.-The-design-should-inc.webp?resize=700%2C400&ssl=1 2x"},"classes":[]},{"id":1406,"url":"https:\/\/oussamasaidi.com\/en\/net-8-test-driven-design-architecture-7-proven-patterns-to-build-robust-maintainable-systems\/","url_meta":{"origin":1254,"position":4},"title":"Net 8 Test Driven Design Architecture 7 Proven Patterns to Build Robust, Maintainable Systems","author":"Saidi Oussama","date":"November 25, 2025","format":false,"excerpt":"Introduction to Test Driven Design \u2014 What this guide covers If you want a battle-tested approach to designing systems that are maintainable, testable, and production-ready, .Net 8 Test Driven Design Architecture combines the stability of .NET 8 with Test Driven Design discipline and modern architecture patterns. This guide gives patterns,\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/11\/dot-net-8-tdd-architecture-article-cover.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":1035,"url":"https:\/\/oussamasaidi.com\/en\/creer-un-chatbot-avec-deepseek-et-net-9-tutoriel-complet\/","url_meta":{"origin":1254,"position":5},"title":"Cr\u00e9er un Chatbot avec DeepSeek et .NET 9 : Tutoriel Complet","author":"Saidi Oussama","date":"April 6, 2025","format":false,"excerpt":"Ce tutoriel vous guidera pas \u00e0 pas pour construire un chatbot utilisant l'API de DeepSeek avec .NET 9. Nous allons cr\u00e9er une application web de chatbot avec ASP.NET Core. Pr\u00e9requis\u00c9tape 1 : Cr\u00e9er une nouvelle application Web ASP.NET Core\u00c9tape 2 : Ajouter les packages n\u00e9cessaires\u00c9tape 3 : Cr\u00e9er les mod\u00e8les\u2026","rel":"","context":"In &quot;.Net Core&quot;","block_context":{"text":".Net Core","link":"https:\/\/oussamasaidi.com\/en\/category\/netcore\/"},"img":{"alt_text":"AI Chatbot with .net core","src":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/04\/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_w4yx4rdp1ohuvcb96ypg.webp?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/04\/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_w4yx4rdp1ohuvcb96ypg.webp?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/04\/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_w4yx4rdp1ohuvcb96ypg.webp?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/oussamasaidi.com\/wp-content\/uploads\/2025\/04\/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_w4yx4rdp1ohuvcb96ypg.webp?resize=700%2C400&ssl=1 2x"},"classes":[]}],"authors":[{"term_id":286,"user_id":1,"is_guest":0,"slug":"oussama_sa","display_name":"Saidi Oussama","avatar_url":{"url":"https:\/\/oussamasaidi.com\/wp-content\/uploads\/2022\/02\/001_001_cv1.jpg","url2x":"https:\/\/oussamasaidi.com\/wp-content\/uploads\/2022\/02\/001_001_cv1.jpg"},"author_category":"1","first_name":"Oussama","last_name":"SAIDI","user_url":"https:\/\/oussamasaidi.com","job_title":"Senior Fullstack .NET Developer","description":"I\u2019m a Senior Fullstack .NET Developer specializing in building scalable, high-performance web applications with .NET, C#, and modern frontend frameworks like React.js. I\u2019m passionate about clean architecture, automated testing, and sharing knowledge through blogs and tutorials."}],"_links":{"self":[{"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/posts\/1254","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/comments?post=1254"}],"version-history":[{"count":9,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/posts\/1254\/revisions"}],"predecessor-version":[{"id":1712,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/posts\/1254\/revisions\/1712"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/media\/1265"}],"wp:attachment":[{"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/media?parent=1254"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/categories?post=1254"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/tags?post=1254"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/oussamasaidi.com\/en\/wp-json\/wp\/v2\/ppma_author?post=1254"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}