From f0adff6c92e3530169783dcbcb2cafe77951ac68 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Mon, 24 Aug 2026 00:35:02 +0200 Subject: [PATCH 1/2] Migrate the Polly resilience sample into CodeMazeGuides on .NET 10 and Polly 8 The sample for "Creating Resilient Microservices in .NET with Polly" lived in the standalone CodeMazeBlog/resilient-microservices-dotnet-polly repository, which predates this repo and therefore sat outside the per-article build check. It targeted .NET 5 and Polly 7.2.1. - dotnet-client-libraries/ResilienceWithPolly with StarterCode and FinishedCode side by side and an aggregate ResilienceWithPolly.sln at the folder root - every project retargeted to net10.0 and converted to minimal hosting - the gateway's v7 policies (AsyncFallbackPolicy, AsyncRetryPolicy, AsyncCircuitBreakerPolicy, AsyncPolicyWrap) rewritten as one Polly 8 ResiliencePipeline, registered with the DI container instead of held in a static field behind a null check - Thread.Sleep(5000) in the Authors failure simulation replaced with await Task.Delay, so the simulation stops demonstrating thread starvation in an article about resilience - Tests project covering the pipeline: the retry recovers one transient failure, the fallback message is returned when every attempt fails, and the circuit opens and short-circuits the dependency --- dotnet-client-libraries/README.md | 1 + .../AuthorsService/AuthorsService.csproj | 9 ++ .../Controllers/AuthorsController.cs | 13 ++ .../AuthorsService/Data/Repository.cs | 34 +++++ .../AuthorsService/Models/Author.cs | 8 ++ .../FinishedCode/AuthorsService/Program.cs | 13 ++ .../Properties/launchSettings.json | 14 ++ .../appsettings.Development.json | 9 ++ .../AuthorsService/appsettings.json | 10 ++ .../BooksService/BooksService.csproj | 9 ++ .../Controllers/BooksController.cs | 13 ++ .../BooksService/Data/Repository.cs | 15 ++ .../FinishedCode/BooksService/Models/Book.cs | 9 ++ .../FinishedCode/BooksService/Program.cs | 13 ++ .../Properties/launchSettings.json | 14 ++ .../BooksService/appsettings.Development.json | 9 ++ .../BooksService/appsettings.json | 10 ++ .../FinishedCode/Monolith/Monolith.csproj | 13 ++ .../FinishedCode/Monolith/Program.cs | 21 +++ .../Monolith/Properties/launchSettings.json | 14 ++ .../FinishedCode/Monolith/ProxyController.cs | 32 +++++ .../Monolith/Resilience/ProxyPipeline.cs | 38 +++++ .../Monolith/appsettings.Development.json | 9 ++ .../FinishedCode/Monolith/appsettings.json | 10 ++ .../FinishedCode/consumer.html | 21 +++ .../ResilienceWithPolly/README.md | 29 ++++ .../ResilienceWithPolly.sln | 130 ++++++++++++++++++ .../AuthorsService/AuthorsService.csproj | 9 ++ .../Controllers/AuthorsController.cs | 13 ++ .../AuthorsService/Data/Repository.cs | 14 ++ .../AuthorsService/Models/Author.cs | 8 ++ .../StarterCode/AuthorsService/Program.cs | 13 ++ .../Properties/launchSettings.json | 14 ++ .../appsettings.Development.json | 9 ++ .../AuthorsService/appsettings.json | 10 ++ .../BooksService/BooksService.csproj | 9 ++ .../Controllers/BooksController.cs | 13 ++ .../BooksService/Data/Repository.cs | 15 ++ .../StarterCode/BooksService/Models/Book.cs | 9 ++ .../StarterCode/BooksService/Program.cs | 13 ++ .../Properties/launchSettings.json | 14 ++ .../BooksService/appsettings.Development.json | 9 ++ .../StarterCode/BooksService/appsettings.json | 10 ++ .../StarterCode/Monolith/Monolith.csproj | 9 ++ .../StarterCode/Monolith/Program.cs | 13 ++ .../Monolith/Properties/launchSettings.json | 14 ++ .../StarterCode/Monolith/ProxyController.cs | 19 +++ .../Monolith/appsettings.Development.json | 9 ++ .../StarterCode/Monolith/appsettings.json | 10 ++ .../StarterCode/consumer.html | 21 +++ .../Tests/ProxyPipelineTests.cs | 117 ++++++++++++++++ .../ResilienceWithPolly/Tests/Tests.csproj | 27 ++++ 52 files changed, 941 insertions(+) create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/AuthorsService.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Controllers/AuthorsController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Data/Repository.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Models/Author.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/BooksService.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Controllers/BooksController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Data/Repository.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Models/Book.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Monolith.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/ProxyController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Resilience/ProxyPipeline.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/FinishedCode/consumer.html create mode 100644 dotnet-client-libraries/ResilienceWithPolly/README.md create mode 100644 dotnet-client-libraries/ResilienceWithPolly/ResilienceWithPolly.sln create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/AuthorsService.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Models/Author.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/BooksService.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Controllers/BooksController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Data/Repository.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Models/Book.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Monolith.csproj create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Program.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Properties/launchSettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/ProxyController.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.Development.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.json create mode 100644 dotnet-client-libraries/ResilienceWithPolly/StarterCode/consumer.html create mode 100644 dotnet-client-libraries/ResilienceWithPolly/Tests/ProxyPipelineTests.cs create mode 100644 dotnet-client-libraries/ResilienceWithPolly/Tests/Tests.csproj diff --git a/dotnet-client-libraries/README.md b/dotnet-client-libraries/README.md index ada6c460ef..1f5b8dfac6 100644 --- a/dotnet-client-libraries/README.md +++ b/dotnet-client-libraries/README.md @@ -58,3 +58,4 @@ This section contains the topics about the client libraries in .NET. - [Multi-Source Data Integration With Strawberry Shake Subscriptions](https://code-maze.com/dotnetcore-multi-source-data-integration-with-strawberry-shake-subscriptions/) - [Introduction to the Wolverine Library in .NET](https://code-maze.com/dotnet-wolverine-library/) - [Comparison of Rebus, NServiceBus, and MassTransit in .NET](https://code-maze.com/aspnetcore-comparison-of-rebus-nservicebus-and-masstransit/) +- [Polly in .NET: Retry, Circuit Breaker, and Fallback](https://code-maze.com/creating-resilient-microservices-in-net-with-polly/) diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/AuthorsService.csproj b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/AuthorsService.csproj new file mode 100644 index 0000000000..c5245308ad --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/AuthorsService.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Controllers/AuthorsController.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Controllers/AuthorsController.cs new file mode 100644 index 0000000000..4dba469a22 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Controllers/AuthorsController.cs @@ -0,0 +1,13 @@ +using AuthorsService.Data; +using AuthorsService.Models; +using Microsoft.AspNetCore.Mvc; + +namespace AuthorsService.Controllers; + +[ApiController] +[Route("[controller]")] +public class AuthorsController(Repository repository) : ControllerBase +{ + [HttpGet] + public Task> Get() => repository.GetAuthorsAsync(); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Data/Repository.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Data/Repository.cs new file mode 100644 index 0000000000..e85b66d322 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Data/Repository.cs @@ -0,0 +1,34 @@ +using AuthorsService.Models; + +namespace AuthorsService.Data; + +public class Repository +{ + private readonly IEnumerable _authors = + [ + new Author { AuthorId = 1, Name = "John Doe", Country = "Australia" }, + new Author { AuthorId = 2, Name = "Jane Smith", Country = "United States" } + ]; + + private readonly DateTime _startTime = DateTime.UtcNow; + private bool _shouldFail = true; + + public async Task> GetAuthorsAsync() + { + if (_shouldFail) + { + _shouldFail = false; + + throw new InvalidOperationException("Oops!"); + } + + if (_startTime.AddMinutes(1) > DateTime.UtcNow) + { + await Task.Delay(TimeSpan.FromSeconds(5)); + + throw new TimeoutException("Timeout!"); + } + + return _authors; + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Models/Author.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Models/Author.cs new file mode 100644 index 0000000000..7862499477 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Models/Author.cs @@ -0,0 +1,8 @@ +namespace AuthorsService.Models; + +public class Author +{ + public int AuthorId { get; set; } + public required string Name { get; set; } + public required string Country { get; set; } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Program.cs new file mode 100644 index 0000000000..5809f0d7ce --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Program.cs @@ -0,0 +1,13 @@ +using AuthorsService.Data; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Properties/launchSettings.json new file mode 100644 index 0000000000..92175fd202 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "AuthorsService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/AuthorsService/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/BooksService.csproj b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/BooksService.csproj new file mode 100644 index 0000000000..c5245308ad --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/BooksService.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Controllers/BooksController.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Controllers/BooksController.cs new file mode 100644 index 0000000000..8b1a70a3d4 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Controllers/BooksController.cs @@ -0,0 +1,13 @@ +using BooksService.Data; +using BooksService.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BooksService.Controllers; + +[ApiController] +[Route("[controller]")] +public class BooksController(Repository repository) : ControllerBase +{ + [HttpGet] + public IEnumerable Get() => repository.GetBooks(); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Data/Repository.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Data/Repository.cs new file mode 100644 index 0000000000..8873a17a6d --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Data/Repository.cs @@ -0,0 +1,15 @@ +using BooksService.Models; + +namespace BooksService.Data; + +public class Repository +{ + private readonly IEnumerable _books = + [ + new Book { BookId = 1, AuthorId = 1, Name = "The Fallen Shore", NumberOfPages = 123 }, + new Book { BookId = 2, AuthorId = 1, Name = "Harmony of Joy", NumberOfPages = 211 }, + new Book { BookId = 3, AuthorId = 2, Name = "Aliens vs Robots", NumberOfPages = 345 } + ]; + + public IEnumerable GetBooks() => _books; +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Models/Book.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Models/Book.cs new file mode 100644 index 0000000000..71bee9ec7e --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Models/Book.cs @@ -0,0 +1,9 @@ +namespace BooksService.Models; + +public class Book +{ + public int BookId { get; set; } + public int AuthorId { get; set; } + public required string Name { get; set; } + public int NumberOfPages { get; set; } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Program.cs new file mode 100644 index 0000000000..7d0bba4d9d --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Program.cs @@ -0,0 +1,13 @@ +using BooksService.Data; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Properties/launchSettings.json new file mode 100644 index 0000000000..880d77cb88 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "BooksService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:6001;http://localhost:6000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/BooksService/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Monolith.csproj b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Monolith.csproj new file mode 100644 index 0000000000..31f84661f6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Monolith.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Program.cs new file mode 100644 index 0000000000..eecdadcd11 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Program.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Mvc; +using Monolith.Resilience; +using Polly; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.AllowAnyOrigin())); +builder.Services.AddHttpClient(); +builder.Services.AddControllers(); + +builder.Services.AddResiliencePipeline( + ProxyPipeline.Name, + (pipeline, _) => ProxyPipeline.Configure(pipeline)); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseCors(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Properties/launchSettings.json new file mode 100644 index 0000000000..b3afac33b9 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Monolith": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:7001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/ProxyController.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/ProxyController.cs new file mode 100644 index 0000000000..9daa900600 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/ProxyController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using Monolith.Resilience; +using Polly; +using Polly.Registry; + +namespace Monolith; + +[Route("[action]")] +[ApiController] +public class ProxyController : ControllerBase +{ + private readonly HttpClient _httpClient; + private readonly ResiliencePipeline _pipeline; + + public ProxyController(IHttpClientFactory httpClientFactory, + ResiliencePipelineProvider pipelineProvider) + { + _httpClient = httpClientFactory.CreateClient(); + _pipeline = pipelineProvider.GetPipeline(ProxyPipeline.Name); + } + + [HttpGet] + public Task Books() => ProxyTo("https://localhost:6001/books"); + + [HttpGet] + public Task Authors() => ProxyTo("https://localhost:5001/authors"); + + private async Task ProxyTo(string url) + => await _pipeline.ExecuteAsync( + async token => (IActionResult)Content(await _httpClient.GetStringAsync(url, token)), + HttpContext.RequestAborted); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Resilience/ProxyPipeline.cs b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Resilience/ProxyPipeline.cs new file mode 100644 index 0000000000..fa06dd9080 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/Resilience/ProxyPipeline.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using Polly; +using Polly.CircuitBreaker; +using Polly.Fallback; +using Polly.Retry; + +namespace Monolith.Resilience; + +public static class ProxyPipeline +{ + public const string Name = "proxy"; + + public const string FallbackMessage = + "Sorry, we are currently experiencing issues. Please try again later"; + + public static void Configure(ResiliencePipelineBuilder builder) => + builder + .AddFallback(new FallbackStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + FallbackAction = static _ => Outcome.FromResultAsValueTask( + new ContentResult { Content = FallbackMessage }) + }) + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + MaxRetryAttempts = 1, + Delay = TimeSpan.Zero + }) + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + FailureRatio = 1.0, + MinimumThroughput = 2, + SamplingDuration = TimeSpan.FromSeconds(30), + BreakDuration = TimeSpan.FromMinutes(1) + }); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/Monolith/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/consumer.html b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/consumer.html new file mode 100644 index 0000000000..208247c342 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/FinishedCode/consumer.html @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/dotnet-client-libraries/ResilienceWithPolly/README.md b/dotnet-client-libraries/ResilienceWithPolly/README.md new file mode 100644 index 0000000000..2967a94f16 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/README.md @@ -0,0 +1,29 @@ +## Polly in .NET: Retry, Circuit Breaker, and Fallback + +Source code for [Polly in .NET: Retry, Circuit Breaker, and Fallback](https://code-maze.com/creating-resilient-microservices-in-net-with-polly/). + +The sample is an API gateway (`Monolith`) in front of two microservices, +`AuthorsService` and `BooksService`. The gateway proxies requests to whichever +service the URL names, so stopping a service is enough to simulate a dependency +failure. + +| Folder | What it is | +| - | - | +| `StarterCode` | The starting point — the gateway and the two services, with no resilience at all. Clone this one first if you want to follow the article step by step. | +| `FinishedCode` | The finished sample — the gateway executes every proxied call through a Polly v8 `ResiliencePipeline` with fallback, retry, and circuit-breaker strategies, and the Authors service simulates transient and slow failures. | +| `Tests` | Tests over the finished pipeline: the retry recovers a single transient failure, the fallback message is returned when every attempt fails, and the circuit opens and stops calling the dependency. | + +Everything targets .NET 10 and Polly 8. + +``` +dotnet build ResilienceWithPolly.sln +dotnet test ResilienceWithPolly.sln +``` + +### Running the sample + +Run all three projects — `AuthorsService` on `https://localhost:5001`, +`BooksService` on `https://localhost:6001`, and `Monolith` on +`https://localhost:7001` — then open `consumer.html` from the same folder in a +browser and use its two buttons. Stopping a service while the page is open is +what produces the failures the article walks through. diff --git a/dotnet-client-libraries/ResilienceWithPolly/ResilienceWithPolly.sln b/dotnet-client-libraries/ResilienceWithPolly/ResilienceWithPolly.sln new file mode 100644 index 0000000000..31a494ec0a --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/ResilienceWithPolly.sln @@ -0,0 +1,130 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "StarterCode", "StarterCode", "{B51ECAE0-892D-0853-1CAF-3BF2C7EB192B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorsService", "StarterCode\AuthorsService\AuthorsService.csproj", "{437AED6E-2BD5-463F-A1C2-529B73F77513}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BooksService", "StarterCode\BooksService\BooksService.csproj", "{AB313368-5534-41E3-BB83-20B60BA33277}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monolith", "StarterCode\Monolith\Monolith.csproj", "{C1DDFC6B-53CD-435E-A70C-28D4224E83F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FinishedCode", "FinishedCode", "{427DD017-79F4-E6E7-884D-AD9B36065000}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthorsService", "FinishedCode\AuthorsService\AuthorsService.csproj", "{39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BooksService", "FinishedCode\BooksService\BooksService.csproj", "{34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Monolith", "FinishedCode\Monolith\Monolith.csproj", "{66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|Any CPU.Build.0 = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|x64.ActiveCfg = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|x64.Build.0 = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|x86.ActiveCfg = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Debug|x86.Build.0 = Debug|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|Any CPU.ActiveCfg = Release|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|Any CPU.Build.0 = Release|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|x64.ActiveCfg = Release|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|x64.Build.0 = Release|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|x86.ActiveCfg = Release|Any CPU + {437AED6E-2BD5-463F-A1C2-529B73F77513}.Release|x86.Build.0 = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|x64.Build.0 = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Debug|x86.Build.0 = Debug|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|Any CPU.Build.0 = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|x64.ActiveCfg = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|x64.Build.0 = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|x86.ActiveCfg = Release|Any CPU + {AB313368-5534-41E3-BB83-20B60BA33277}.Release|x86.Build.0 = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|x64.ActiveCfg = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|x64.Build.0 = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Debug|x86.Build.0 = Debug|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|Any CPU.Build.0 = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|x64.ActiveCfg = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|x64.Build.0 = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|x86.ActiveCfg = Release|Any CPU + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8}.Release|x86.Build.0 = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|x64.ActiveCfg = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|x64.Build.0 = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|x86.ActiveCfg = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Debug|x86.Build.0 = Debug|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|Any CPU.Build.0 = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|x64.ActiveCfg = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|x64.Build.0 = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|x86.ActiveCfg = Release|Any CPU + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B}.Release|x86.Build.0 = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|x64.ActiveCfg = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|x64.Build.0 = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|x86.ActiveCfg = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Debug|x86.Build.0 = Debug|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|Any CPU.Build.0 = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|x64.ActiveCfg = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|x64.Build.0 = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|x86.ActiveCfg = Release|Any CPU + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A}.Release|x86.Build.0 = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|x64.Build.0 = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Debug|x86.Build.0 = Debug|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|Any CPU.Build.0 = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|x64.ActiveCfg = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|x64.Build.0 = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|x86.ActiveCfg = Release|Any CPU + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD}.Release|x86.Build.0 = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|x64.ActiveCfg = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|x64.Build.0 = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|x86.ActiveCfg = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Debug|x86.Build.0 = Debug|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|Any CPU.Build.0 = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|x64.ActiveCfg = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|x64.Build.0 = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|x86.ActiveCfg = Release|Any CPU + {9A2E4D71-6750-4A11-9ABA-6BE6F1E0EF22}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {437AED6E-2BD5-463F-A1C2-529B73F77513} = {B51ECAE0-892D-0853-1CAF-3BF2C7EB192B} + {AB313368-5534-41E3-BB83-20B60BA33277} = {B51ECAE0-892D-0853-1CAF-3BF2C7EB192B} + {C1DDFC6B-53CD-435E-A70C-28D4224E83F8} = {B51ECAE0-892D-0853-1CAF-3BF2C7EB192B} + {39DF074F-A20D-45C1-AE3F-BB4CD36CBB6B} = {427DD017-79F4-E6E7-884D-AD9B36065000} + {34B9EC20-6B29-43C2-83FB-A2B5DDA65E6A} = {427DD017-79F4-E6E7-884D-AD9B36065000} + {66EA99B8-AC93-4DC3-B1C6-C08C01B541DD} = {427DD017-79F4-E6E7-884D-AD9B36065000} + EndGlobalSection +EndGlobal diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/AuthorsService.csproj b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/AuthorsService.csproj new file mode 100644 index 0000000000..c5245308ad --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/AuthorsService.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs new file mode 100644 index 0000000000..4dba469a22 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs @@ -0,0 +1,13 @@ +using AuthorsService.Data; +using AuthorsService.Models; +using Microsoft.AspNetCore.Mvc; + +namespace AuthorsService.Controllers; + +[ApiController] +[Route("[controller]")] +public class AuthorsController(Repository repository) : ControllerBase +{ + [HttpGet] + public Task> Get() => repository.GetAuthorsAsync(); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs new file mode 100644 index 0000000000..8a85448aa7 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs @@ -0,0 +1,14 @@ +using AuthorsService.Models; + +namespace AuthorsService.Data; + +public class Repository +{ + private readonly IEnumerable _authors = + [ + new Author { AuthorId = 1, Name = "John Doe", Country = "Australia" }, + new Author { AuthorId = 2, Name = "Jane Smith", Country = "United States" } + ]; + + public Task> GetAuthorsAsync() => Task.FromResult(_authors); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Models/Author.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Models/Author.cs new file mode 100644 index 0000000000..7862499477 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Models/Author.cs @@ -0,0 +1,8 @@ +namespace AuthorsService.Models; + +public class Author +{ + public int AuthorId { get; set; } + public required string Name { get; set; } + public required string Country { get; set; } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Program.cs new file mode 100644 index 0000000000..5809f0d7ce --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Program.cs @@ -0,0 +1,13 @@ +using AuthorsService.Data; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Properties/launchSettings.json new file mode 100644 index 0000000000..92175fd202 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "AuthorsService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/BooksService.csproj b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/BooksService.csproj new file mode 100644 index 0000000000..c5245308ad --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/BooksService.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Controllers/BooksController.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Controllers/BooksController.cs new file mode 100644 index 0000000000..8b1a70a3d4 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Controllers/BooksController.cs @@ -0,0 +1,13 @@ +using BooksService.Data; +using BooksService.Models; +using Microsoft.AspNetCore.Mvc; + +namespace BooksService.Controllers; + +[ApiController] +[Route("[controller]")] +public class BooksController(Repository repository) : ControllerBase +{ + [HttpGet] + public IEnumerable Get() => repository.GetBooks(); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Data/Repository.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Data/Repository.cs new file mode 100644 index 0000000000..8873a17a6d --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Data/Repository.cs @@ -0,0 +1,15 @@ +using BooksService.Models; + +namespace BooksService.Data; + +public class Repository +{ + private readonly IEnumerable _books = + [ + new Book { BookId = 1, AuthorId = 1, Name = "The Fallen Shore", NumberOfPages = 123 }, + new Book { BookId = 2, AuthorId = 1, Name = "Harmony of Joy", NumberOfPages = 211 }, + new Book { BookId = 3, AuthorId = 2, Name = "Aliens vs Robots", NumberOfPages = 345 } + ]; + + public IEnumerable GetBooks() => _books; +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Models/Book.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Models/Book.cs new file mode 100644 index 0000000000..71bee9ec7e --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Models/Book.cs @@ -0,0 +1,9 @@ +namespace BooksService.Models; + +public class Book +{ + public int BookId { get; set; } + public int AuthorId { get; set; } + public required string Name { get; set; } + public int NumberOfPages { get; set; } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Program.cs new file mode 100644 index 0000000000..7d0bba4d9d --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Program.cs @@ -0,0 +1,13 @@ +using BooksService.Data; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Properties/launchSettings.json new file mode 100644 index 0000000000..880d77cb88 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "BooksService": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:6001;http://localhost:6000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/BooksService/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Monolith.csproj b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Monolith.csproj new file mode 100644 index 0000000000..c5245308ad --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Monolith.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Program.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Program.cs new file mode 100644 index 0000000000..802fa2abb2 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Program.cs @@ -0,0 +1,13 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.AllowAnyOrigin())); +builder.Services.AddHttpClient(); +builder.Services.AddControllers(); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseCors(); +app.MapControllers(); + +app.Run(); diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Properties/launchSettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Properties/launchSettings.json new file mode 100644 index 0000000000..b3afac33b9 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Monolith": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "", + "applicationUrl": "https://localhost:7001", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/ProxyController.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/ProxyController.cs new file mode 100644 index 0000000000..96b941f0d3 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/ProxyController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Monolith; + +[Route("[action]")] +[ApiController] +public class ProxyController(IHttpClientFactory httpClientFactory) : ControllerBase +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(); + + [HttpGet] + public Task Books() => ProxyTo("https://localhost:6001/books"); + + [HttpGet] + public Task Authors() => ProxyTo("https://localhost:5001/authors"); + + private async Task ProxyTo(string url) + => Content(await _httpClient.GetStringAsync(url)); +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.Development.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.Development.json new file mode 100644 index 0000000000..8983e0fc1c --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.json b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.json new file mode 100644 index 0000000000..d9d9a9bff6 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/Monolith/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/consumer.html b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/consumer.html new file mode 100644 index 0000000000..c7990d2fc2 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/consumer.html @@ -0,0 +1,21 @@ + + + + + + + + \ No newline at end of file diff --git a/dotnet-client-libraries/ResilienceWithPolly/Tests/ProxyPipelineTests.cs b/dotnet-client-libraries/ResilienceWithPolly/Tests/ProxyPipelineTests.cs new file mode 100644 index 0000000000..6b568f9be9 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/Tests/ProxyPipelineTests.cs @@ -0,0 +1,117 @@ +using Microsoft.AspNetCore.Mvc; +using Monolith.Resilience; +using Polly; +using Polly.CircuitBreaker; + +namespace Tests; + +[TestClass] +public class ProxyPipelineTests +{ + private static ResiliencePipeline BuildPipeline() + { + var builder = new ResiliencePipelineBuilder(); + ProxyPipeline.Configure(builder); + + return builder.Build(); + } + + private static string? ContentOf(IActionResult result) => + ((ContentResult)result).Content; + + [TestMethod] + public async Task GivenACallThatSucceeds_WhenExecutedThroughThePipeline_ThenItReturnsTheResult() + { + var pipeline = BuildPipeline(); + + var result = await pipeline.ExecuteAsync(static _ => + ValueTask.FromResult(new ContentResult { Content = "authors" })); + + Assert.AreEqual("authors", ContentOf(result)); + } + + [TestMethod] + public async Task GivenACallThatFailsOnce_WhenExecutedThroughThePipeline_ThenTheRetryRecoversIt() + { + var pipeline = BuildPipeline(); + var attempts = 0; + + var result = await pipeline.ExecuteAsync(_ => + { + attempts++; + + if (attempts == 1) + { + throw new InvalidOperationException("Oops!"); + } + + return ValueTask.FromResult(new ContentResult { Content = "authors" }); + }); + + Assert.AreEqual(2, attempts); + Assert.AreEqual("authors", ContentOf(result)); + } + + [TestMethod] + public async Task GivenACallThatAlwaysFails_WhenExecutedThroughThePipeline_ThenTheFallbackMessageIsReturned() + { + var pipeline = BuildPipeline(); + + var result = await pipeline.ExecuteAsync(static _ => + throw new InvalidOperationException("Oops!")); + + Assert.AreEqual(ProxyPipeline.FallbackMessage, ContentOf(result)); + } + + [TestMethod] + public async Task GivenACallThatAlwaysFails_WhenTheThroughputThresholdIsReached_ThenTheCircuitOpensAndStopsCallingTheDependency() + { + var pipeline = BuildPipeline(); + var attempts = 0; + + // One execution is two attempts -- the call plus a single retry -- which + // meets MinimumThroughput at a failure ratio of 1.0, so the circuit opens. + await pipeline.ExecuteAsync(_ => + { + attempts++; + + throw new InvalidOperationException("Oops!"); + }); + + Assert.AreEqual(2, attempts); + + var result = await pipeline.ExecuteAsync(_ => + { + attempts++; + + throw new InvalidOperationException("Oops!"); + }); + + Assert.AreEqual(2, attempts, "The open circuit must short-circuit without calling the dependency again."); + Assert.AreEqual(ProxyPipeline.FallbackMessage, ContentOf(result)); + } + + [TestMethod] + public async Task GivenAnOpenCircuit_WhenTheFallbackIsRemoved_ThenABrokenCircuitExceptionSurfaces() + { + var pipeline = new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle(), + FailureRatio = 1.0, + MinimumThroughput = 2, + SamplingDuration = TimeSpan.FromSeconds(30), + BreakDuration = TimeSpan.FromMinutes(1) + }) + .Build(); + + for (var i = 0; i < 2; i++) + { + await Assert.ThrowsExactlyAsync(async () => + await pipeline.ExecuteAsync(static _ => throw new InvalidOperationException("Oops!"))); + } + + await Assert.ThrowsExactlyAsync(async () => + await pipeline.ExecuteAsync(static _ => ValueTask.CompletedTask)); + } +} diff --git a/dotnet-client-libraries/ResilienceWithPolly/Tests/Tests.csproj b/dotnet-client-libraries/ResilienceWithPolly/Tests/Tests.csproj new file mode 100644 index 0000000000..bd887ec033 --- /dev/null +++ b/dotnet-client-libraries/ResilienceWithPolly/Tests/Tests.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + From e060e6be41ef19992c488c61fa08df6fec3dbb96 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Mon, 24 Aug 2026 00:39:04 +0200 Subject: [PATCH 2/2] ResilienceWithPolly: keep the starter Authors repository synchronous The starter project has no failure simulation, so there is nothing to await. The async signature arrives in the finished project along with the Task.Delay that replaces Thread.Sleep, which is where the article introduces it. --- .../StarterCode/AuthorsService/Controllers/AuthorsController.cs | 2 +- .../StarterCode/AuthorsService/Data/Repository.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs index 4dba469a22..f665c8dcf8 100644 --- a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Controllers/AuthorsController.cs @@ -9,5 +9,5 @@ namespace AuthorsService.Controllers; public class AuthorsController(Repository repository) : ControllerBase { [HttpGet] - public Task> Get() => repository.GetAuthorsAsync(); + public IEnumerable Get() => repository.GetAuthors(); } diff --git a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs index 8a85448aa7..0e04b31cc3 100644 --- a/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs +++ b/dotnet-client-libraries/ResilienceWithPolly/StarterCode/AuthorsService/Data/Repository.cs @@ -10,5 +10,5 @@ public class Repository new Author { AuthorId = 2, Name = "Jane Smith", Country = "United States" } ]; - public Task> GetAuthorsAsync() => Task.FromResult(_authors); + public IEnumerable GetAuthors() => _authors; }