Saturday, 23 August 2025

Recovering lost files in Git after hard resets

πŸ”§ How I Recovered a Lost File After git reset --hard in Git

Disclaimer: You are not guaranteed that you can recover the lost file from your Git repo's dangling blogs, much of this information is kept only for a few weeks. Therefore, you should run recovery of a lost file in Git repo as soon as possible or within a few weeks.

Have you ever added a file in Git, only to lose it after running git reset --hard? I did — and I managed to recover it using a lesser-known Git command: git fsck.

Here’s how it happened and how I got my file back.

πŸ’₯ The Mistake

I created a file called fil2.txt, added it to Git with:

git add fil2.txt

But before committing it, I ran:

git reset --hard

This command wipes out all uncommitted changes, including files that were staged but not yet committed. My file was gone from the working directory and the staging area.

πŸ•΅️‍♂️ The Recovery Trick: git fsck

Thankfully, Git doesn’t immediately delete everything. It keeps unreferenced objects (like blobs) around for a while. You can find them using:

git fsck --lost-found

This listed several dangling blobs — unreferenced file contents:

dangling blob fb48af767fd2271a9978045a971c2eee199b03b7

πŸ” Finding the Right Blob

To inspect the contents of a blob, I used:

git show fb48af767fd2271a9978045a971c2eee199b03b7

It printed:

dette er en fil som jeg fil recovere

That was my lost file!

πŸ’Ύ Restoring the File

To recover it, I simply redirected the blob’s contents back into a file:

git show fb48af767fd2271a9978045a971c2eee199b03b7 > fil2.txt

And just like that, fil2.txt was back in my working directory.

✅ Lesson Learned

  • git reset --hard is powerful — and dangerous.
  • If you lose a file, don’t panic. Try git fsck --lost-found.
  • You might be able to recover your work — even if it was never committed.

Additional Tips - More detailed dangling objecst information

Git Alias: Dangling Object Summary

This Git alias defines a shell function that summarizes dangling objects in your repository, showing type, SHA, commit metadata, and blob previews.

[alias]
    dangling-summary = "!sh -c '\
        summarize_dangling() { \
            git fsck --full | grep dangling | while read -r _ type sha; do \
                echo -e \"\\033[1;33mType:\\033[0m $type\"; \
                echo -e \"\\033[1;33mSHA:\\033[0m $sha\"; \
                case $type in \
                    commit) \
                        author=$(git show -s --format=%an $sha); \
                        date=$(git show -s --format=%ci $sha); \
                        msg=$(git show -s --format=%s $sha); \
                        echo -e \"\\033[1;33mAuthor:\\033[0m $author\"; \
                        echo -e \"\\033[1;33mDate:\\033[0m $date\"; \
                        echo -e \"\\033[0;32mMessage:\\033[0m $msg\" ;; \
                    blob) \
                        preview=$(git cat-file -p $sha | head -n 3 | cut -c1-80); \
                        echo -e \"\\033[0;32mPreview:\\033[0m\\n$preview\" ;; \
                esac; \
                echo -e \"-----------------------------\"; \
            done; \
        }; \
        summarize_dangling'"
Screenshot showing the detailed dangling objects summary :

πŸ’‘ Tips for Using This Alias

  • Run git dangling-summary inside any Git repository to inspect unreachable objects.
  • Useful for recovering lost commits or inspecting orphaned blobs.
  • Blob previews are limited to 3 lines, each up to 80 characters wide for readability.
  • Commit metadata includes author and timestamp for better context.
  • If you did not commit the file, just added it to your Git repo and then for example did a hard reset or in some other way lost the file(s), there will not be shown any date and author here. The example screenshot above shows an example of this. If you DID commit, author and date will show up.

Saturday, 9 August 2025

Testing API resilience with Polly Chaos engine

Polly is a transient failure handling and resilience library that makes it convenient to build robust APIs based on policies for handling errors that occur and offer different resilience strategies for handling these errors. The errors are not only of errors occurings either externall or internally in API, but offer also alternative strategies such as fallbacks, rate-limiting, circuit breakers and other overall act upon either reactively or proactively. A great overview of Polly can be seen in this video, although some years old now - back to 2019 - of getting an overview of Polly : NDC Oslo 2019 - System Stable: Robust connected applications with Polly, the .NET Resilience Framework - Bryan Hogan With Polly, it is possible to test out API resilience with the built in Polly Chaos engine. The Chaos engine was previously offered via the Simmy library.

Simmy - Logo



The source code in this article is available in my Github repo here: Note - the code shown in the methods below are called from Program.cs to be able to be used in the API. The sample app is an Asp.net application written with C# and with .NET 8 Target Framework. https://github.com/toreaurstadboss/HttpClientUsingPolly/

Testing out API resilience with fallbacking API endpoints

First off, the fallback strategy resilience. Polly offers a way to define fallback policies. Let's look at a way to define an HTTP client that will provide a fallback if the statuscode from the endpoint is InternalServerError = 501. The fallback is just a Json payload in this simple example.

PollyExtensions.cs



    public static void AddPollyHttpClientWithFallback(this IServiceCollection services)
    {
        services.AddHttpClient(Constants.HttpClientNames.FallbackHttpClientName, client =>
        {
            client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
        })
        .AddResilienceHandler(
             $"{FallbackHttpClientName}{ResilienceHandlerSuffix}",
            (builder, context) =>
        {
            var serviceProvider = services.BuildServiceProvider();
            var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

            builder.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
            {
                ShouldHandle = args =>
                {
                    // Fallback skal trigges ved status 500 eller exception
                    return ValueTask.FromResult(
                        args.Outcome.Result?.StatusCode == HttpStatusCode.InternalServerError ||
                        args.Outcome.Exception is HttpRequestException
                    );
                },
                FallbackAction = args =>
                {
                    logger.LogWarning("Fallback triggered. Returning default response.");

                    var jsonObject = new
                    {
                        message = "Fallback response",
                        source = "Polly fallback",
                        timestamp = DateTime.UtcNow
                    };

                    var json = JsonSerializer.Serialize(jsonObject);

                    var fallbackResponse = new HttpResponseMessage(HttpStatusCode.OK)
                    {
                        Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
                    };

                    return ValueTask.FromResult(Outcome.FromResult(fallbackResponse));
                }
            });

            // Inject exceptions in 80% of requests
            builder.AddChaosOutcome(new ChaosOutcomeStrategyOptions<HttpResponseMessage>()
            {
                Enabled = true,
                OutcomeGenerator = static args =>
                {
                    var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                    return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                },
                InjectionRate = 0.8,
                OnOutcomeInjected = args =>
                {
                    logger.LogWarning("Outcome returning internal server error");
                    return default;
                }
            });

        });
    }


Next up, let's look at the client endpoint defined with Minimal API in Aspnet core.

SampleEndpoints.cs



 app.MapGet("/test-v5-fallback", async (
 [FromServices] IHttpClientFactory httpClientFactory) =>
 {
     using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.FallbackHttpClientName);

     HttpResponseMessage? response = await client.GetAsync("https://example.com");

     if (!response.IsSuccessStatusCode)
     {
         var errorContent = await response.Content.ReadAsStringAsync();
         return Results.Problem(
             detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
             statusCode: (int)response.StatusCode,
             title: "External API Error"
         );
     }

     var json = await response!.Content.ReadAsStringAsync();
     return Results.Json(json);

 });



Note the usage of [FromServices] attribute and IHttpClientFactory. The code creates the named Http client defined earlier. The fallback will return a json with fallback content in 80% of the requests in this concrete example.


Testing out API resilience with circuit breaking API endpoints


Next, the circuit breaker strategy for API resilience. Polly offers a way to define circuit breaker policies. Let's look at a way to define an HTTP client that will provide a circuit breaker if it fails for 3 consecutive requests within 30 seconds, resulting in a 10 second break. The circuit breaker strategy will stop requests that opens the circuit defined here. After the break, the circuit breaker half opens. It will accept new request, but fail immediately and open up the circuit again if it fails again, further postponing.

PollyExtensions.cs



  public static void AddPollyHttpClientWithExceptionChaosAndBreaker(this IServiceCollection services)
  {

      services.AddHttpClient(CircuitBreakerHttpClientName, client =>
      {
          client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
      })
      .AddResilienceHandler(
          $"{CircuitBreakerHttpClientName}{ResilienceHandlerSuffix}",
          (builder, context) =>
      {
          var serviceProvider = services.BuildServiceProvider();
          var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

          //Add circuit breaker that opens after three consecutive failures and breaks for a duration of ten seconds
          builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
          {
              MinimumThroughput = 3, //number of CONSECUTIVE requests failing for circuit to open (short-circuiting future requests until given BreakDuration is passed)
              FailureRatio = 1.0, //usually 1.0 is used here..
              SamplingDuration = TimeSpan.FromSeconds(30), //time window duration to look for CONSECUTIVE requests failing
              BreakDuration = TimeSpan.FromSeconds(10), //break duration. requests will be hindered at during this duration set 
              ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                  .HandleResult(r => !r.IsSuccessStatusCode)
                  .Handle<HttpRequestException>(), //defining when circuit breaker will occur given other conditions also apply
              OnOpened = args =>
              {
                  logger.LogInformation("Circuit breaker opened");
                  return default;
              },
              OnClosed = args =>
              {
                  logger.LogInformation("Circuit breaker closed");
                  return default;
              },
              OnHalfOpened = args =>
              {
                  logger.LogInformation("Circuit breaker half opened"); //half opened state happens after the circuit has been opened and break duration has passed, entering 'half-open' state (usually ONE test call must succeeed to transition from half open to open state)
                  return default;
              }
          });


          // Inject exceptions in 80% of requests
          builder.AddChaosOutcome(new ChaosOutcomeStrategyOptions<HttpResponseMessage>()
          {
              Enabled = true,
              OutcomeGenerator = static args =>
              {
                  var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                  return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
              },
              InjectionRate = 0.8,
              OnOutcomeInjected = args =>
              {
                  logger.LogWarning("Outcome returning internal server error");
                  return default;
              }
          });


      });
  }



Let's look at the client endpoint defined with Minimal API in Aspnet core for circuit-breaker example.

SampleEndpoints.cs



  app.MapGet("/test-v4-circuitbreaker-opening", async (
  [FromServices] IHttpClientFactory httpClientFactory) =>
  {
      using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.CircuitBreakerHttpClientName);

      HttpResponseMessage? response = await client.GetAsync("https://example.com");

      if (!response.IsSuccessStatusCode)
      {
          var errorContent = await response.Content.ReadAsStringAsync();
          return Results.Problem(
              detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
              statusCode: (int)response.StatusCode,
              title: "External API Error"
          );
      }

      var json = await response!.Content.ReadAsStringAsync();
      return Results.Json(json);

  });



Testing out API resilience for latency induced timeout API endpoints

Next, the timeout strategy for API resilience. Polly offers a way to define timeout policies and can combine these for testing by injecting latency (additional execution time). Let's look at a way to define an HTTP client that will provide a timeout if it times out already after one second with a 50% chance of getting a 3 second latency, which will trigger the timeout.

PollyExtensions.cs



   public static void AddPollyHttpClientWithIntendedRetriesAndLatencyAndTimeout(this IServiceCollection services)
  {
      services.AddHttpClient(RetryingTimeoutLatencyHttpClientName, client =>
      {
          client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
      })
      .AddResilienceHandler(
          $"{RetryingTimeoutLatencyHttpClientName}{ResilienceHandlerSuffix}",
      (builder, context) =>
       {
           var serviceProvider = services.BuildServiceProvider();
           var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

           // Timeout strategy : fail if request takes longer than 1s
           builder.AddTimeout(new HttpTimeoutStrategyOptions
           {
               Timeout = TimeSpan.FromSeconds(1),
               OnTimeout = args =>
               {
                   logger.LogWarning($"Timeout after {args.Timeout.TotalSeconds} seconds");
                   return default;
               }
           });

           // Chaos latency: inject 3s delay in 30% of cases
           builder.AddChaosLatency(new ChaosLatencyStrategyOptions
           {
               InjectionRate = 0.5,
               Latency = TimeSpan.FromSeconds(3),
               Enabled = true,
               OnLatencyInjected = args =>
               {
                   logger.LogInformation("... Injecting a latency of 3 seconds ...");
                   return default;
               }
           });

           // Chaos strategy: inject 500 Internal Server Error in 75% of cases
           builder.AddChaosOutcome<HttpResponseMessage>(
               new ChaosOutcomeStrategyOptions<HttpResponseMessage>
               {
                   InjectionRate = 0.5,
                   Enabled = true,
                   OutcomeGenerator = static args =>
                   {
                       var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                       return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                   },
                   OnOutcomeInjected = args =>
                   {
                       logger.LogWarning("Outcome returning internal server error");
                       return default;
                   }
               });
       });

  }


Let's look at the client endpoint defined with Minimal API in Aspnet core for timeout with latency example.

SampleEndpoints.cs



 app.MapGet("/test-v3-latency-timeout", async (
 [FromServices] IHttpClientFactory httpClientFactory) =>
 {
     using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.RetryingTimeoutLatencyHttpClientName);

     var response = await client.GetAsync("https://example.com");

     if (!response.IsSuccessStatusCode)
     {
         var errorContent = await response.Content.ReadAsStringAsync();
         return Results.Problem(
             detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
             statusCode: (int)response.StatusCode,
             title: "External API Error"
         );
     }

     var json = await response.Content.ReadAsStringAsync();
     return Results.Json(json);

 });



The following screenshot shows the timeout occuring after the defined setup of induced latency by given probability and defined timeout.

Testing out API resilience with retries

Retries offers an API endpoint to gain more robustness, by allowing multiple retries and define a strategy for these retries.The example http client here also adds a chaos outcome internal server error = 501 that is thrown with 75% probability (failure rate).

PollyExtensions.cs



    public static void AddPollyHttpClientWithIntendedRetries(this IServiceCollection services)
   {
       services.AddHttpClient(RetryingHttpClientName, client =>
           {
               client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
           })
           .AddResilienceHandler("polly-chaos", (builder, context) =>
           {
               var serviceProvider = services.BuildServiceProvider();
               var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

               //Retry strategy
               builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
               {
                   ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                       .HandleResult(r => !r.IsSuccessStatusCode)
                       .Handle<HttpRequestException>(),
                   MaxRetryAttempts = 3,
                   DelayGenerator = RetryDelaysPipeline,
                   OnRetry = args =>
                   {
                       logger.LogWarning($"Retrying {args.AttemptNumber} for requesturi {args.Context.GetRequestMessage()?.RequestUri}");
                       return default;
                   }
               });

               // Chaos strategy: inject 500 Internal Server Error in 75% of cases
               builder.AddChaosOutcome<HttpResponseMessage>(
                   new ChaosOutcomeStrategyOptions<HttpResponseMessage>
                   {
                       InjectionRate = 0.75,
                       Enabled = true,
                       OutcomeGenerator = static args =>
                       {
                           var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                           return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                       }
                   });

           });

   }


Let's look at the client endpoint defined with Minimal API in Aspnet core for the retrying example.

SampleEndpoints.cs


   app.MapGet("/test-retry-v2", async (
       [FromServices] IHttpClientFactory httpClientFactory) =>
   {
       using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.RetryingHttpClientName);

       var response = await client.GetAsync("https://example.com");

       return Results.Json(response);
   });


There are multiple resilience scenarios that Polly offers, the table below lists them up (this article has presented most of them):

Summary

The following summary explains what this article has presented.

πŸ§ͺ Testing API Resilience with Polly Chaos Engineering In this article, we have explored how to build resilient APIs using Polly v9 and its integrated chaos engine. Through practical examples, the article has demonstrated how to simulate real-world failures—like latency, timeouts, and internal server errors—and apply resilience strategies such as fallbacks, retries, and circuit breakers. By injecting controlled chaos, developers can proactively test and strengthen their systems against instability and external dependencies. Polly v9 library offers additionally scenarios also for making robust APIs. Info: Asp net core is used in this article, together with C# and .NET 8 as Target Framework.

Tuesday, 5 August 2025

Asp.Net - Using Polly .NET Resiliency V9

This article explains how using Polly .NET Resiliency library to add support for different resiliency strategies in Asp.net Core. The Polly library is created by Microsoft Community and its site is here:

https://www.pollydocs.org/

Important note about Polly library

The Polly library has changed its API quite a bit from v7 to v8 and now to v9. While Polly has been a Microsoft Community project, Microsoft now takes a bit more lead with Polly v9 being more tightly coupled to Microsoft's ecosystem. Future v10 is expected to be not so much changed as earlier major versions of Polly, only stabilize and
improve. The following Nuget libraries are added to the demo solution, SwashBuckle Nuget also added here for Swagger support used when testing.

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.7" />
  <PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" />
  <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
</ItemGroup>



The source code shown in this article is available to clone from my Github repo here: https://github.com/toreaurstadboss/HttpClientUsingPolly

Extension methods to add Polly v9 support http clients

The following extension methods offers creating a HttpClient via IHttpClientFactory or via a pipeline provided by ResiliencyProvider. Note that only the retry resilience strategy is shown here. There are multiple strategies available with Polly.

PollyExtensions.cs



using Microsoft.Extensions.Http.Resilience;
using Microsoft.Extensions.Logging;
using Polly;
using Polly.Retry;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;

namespace HttpClientUsingPolly
{

    /// <summary>
    /// Contains helper methods to add support for Polly V9 resilience strategies
    /// </summary>
    public static class PollyExtensions
    {
      
        public static void AddPollyHttpClient(this IServiceCollection services)
        {
            services.AddHttpClient(GithubEndpoints.HttpClientName, client =>
                {
                    client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
                })
                .ConfigurePrimaryHttpMessageHandler(() =>
                {
                    var handler = new RandomHttpErrorHandler(errorChance: 75);
                    handler.InnerHandler = new HttpClientHandler(); // Assign the terminal handler
                    return handler;
                }) //IMPORTANT to make Polly retry here using ConfigurePrimaryHttpMessageHandler and set the Innerhandler to HttpClientHandler
                .AddResilienceHandler(RetryResiliencePolicy, builder =>
                {
                    var serviceProvider = services.BuildServiceProvider();
                    var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("NamedPolly");

                    var options = CreateRetryStrategyOptions(logger);
                    builder.AddRetry(options);
                });
        }    

        public static void AddNamedPollyPipelines(this IServiceCollection services)
        {
            services.AddResiliencePipeline<string>(RetryResiliencePolicy, builder =>
            {
                var serviceProvider = services.BuildServiceProvider();
                var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("NamedPolly");

                builder.AddRetry(new RetryStrategyOptions
                {
                    MaxRetryAttempts = 3,
                    DelayGenerator = RetryDelaysClient,
                    ShouldHandle = new PredicateBuilder().Handle<HttpRequestException>(),
                    OnRetry = args =>
                    {
                        var httpEx = args.Outcome.Exception as HttpRequestException;
                        logger.LogInformation($"[NamedPolicy] Retrying due to: {httpEx?.Message}. Attempt: {args.AttemptNumber}");
                        return default;
                    }
                });
            });
        }

        private static HttpRetryStrategyOptions CreateRetryStrategyOptions(ILogger logger)
        {
            HttpRetryStrategyOptions options = new HttpRetryStrategyOptions
            {
                MaxRetryAttempts = 3,
                DelayGenerator = RetryDelaysPipeline,
                ShouldHandle = args =>
                {
                    if (args.Outcome.Exception is HttpRequestException ||
                        args.Outcome.Exception is TaskCanceledException)
                        return ValueTask.FromResult(true);

                    if (args.Outcome.Result is HttpResponseMessage response &&
                        TransientStatusCodes.Contains(response.StatusCode))
                        return ValueTask.FromResult(true);

                    return ValueTask.FromResult(false);
                },
                OnRetry = args =>
                {
                    logger.LogInformation($"Retrying... Attempt {args.AttemptNumber}");
                    return default;
                }
            };

            return options;
        }

        public const string RetryResiliencePolicy = "RetryResiliencePolicy";

        static readonly HttpStatusCode[] TransientStatusCodes = new[]
        {
            HttpStatusCode.RequestTimeout,      // 408
            HttpStatusCode.InternalServerError, // 500
            HttpStatusCode.BadGateway,          // 502
            HttpStatusCode.ServiceUnavailable,  // 503
            HttpStatusCode.GatewayTimeout       // 504
        };

        static Func<RetryDelayGeneratorArguments<HttpResponseMessage>, ValueTask<TimeSpan?>>? RetryDelaysPipeline =
             args => CommonDelayGenerator(args.AttemptNumber);

        static Func<RetryDelayGeneratorArguments<object>, ValueTask<TimeSpan?>>? RetryDelaysClient =
             args => CommonDelayGenerator(args.AttemptNumber);


        static ValueTask<TimeSpan?> CommonDelayGenerator(int attemptNumber)
        {
            var delay = attemptNumber switch
            {
                1 => TimeSpan.FromSeconds(1),
                2 => TimeSpan.FromSeconds(2),
                3 => TimeSpan.FromSeconds(4),
                _ => TimeSpan.FromSeconds(0) // fallback, shouldn't hit
            };
            return new ValueTask<TimeSpan?>(delay);
        }

    }
}



Note that the use of ConfigurePrimaryHttpMessageHandler here took a lot of time to find out. The Polly docs did not offer a good example of using IHttpClientFactory combined with throwing errors, which will be shown later in this article in the RandomHttpErrorHandler DelegatingHandler. It is also important to configure the primary http message handler here before the resilience handler is added using AddResilienceHandler. The following summary of resilience strategies shows that Polly offers more strategies than just the retry strategy. Every resilience strategy here can be combined to offer multiple resilience strategy to obtain the desired level of QoS = Quality of Service.
Polly Resilience Strategies
Strategy Description Can Be Combined?
Retry Retries a failed operation based on rules (e.g., delay, max attempts). ✅ Yes
Circuit Breaker Stops calls when failures exceed a threshold, allowing time to recover. ✅ Yes
Timeout Limits how long an operation can run before it's considered failed. ✅ Yes
Bulkhead Limits concurrent executions to prevent overload. ✅ Yes
Fallback Provides an alternative result or action when the primary fails. ✅ Yes
Rate Limiter Controls the rate of requests to avoid overwhelming a service. ✅ Yes


Adding support to test out random http request errors

To verify that the retry resilience strategy works, the following DelegatingHandler shown in the code in PollyExtensions will random throw HttpRequestException with a set estimated failure rate in percentage to be able to to end-to-end testing of retries. Debugging the endpoints to be defined later in this article shows the retries are working with this setup.

RandomHttpErrorHandler.cs



namespace HttpClientUsingPolly
{

    public class RandomHttpErrorHandler : DelegatingHandler
    {

        private readonly Random _random = new();
        private readonly double _errorChance;

        public RandomHttpErrorHandler(double errorChance)
        {
            _errorChance = errorChance;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (_random.NextDouble() < (_errorChance / 100))
            {
                // Pick a random transient status code
                var httpErrorsPossible = new[]
                {
                        System.Net.HttpStatusCode.RequestTimeout,        // 408
                        System.Net.HttpStatusCode.InternalServerError,   // 500
                        System.Net.HttpStatusCode.BadGateway,            // 502
                        System.Net.HttpStatusCode.ServiceUnavailable,    // 503
                        System.Net.HttpStatusCode.GatewayTimeout         // 504
                    };

                var chosenStatus = httpErrorsPossible[_random.Next(httpErrorsPossible.Length)];

                throw new HttpRequestException($"Simulated Http error: {(int)chosenStatus}", null, chosenStatus);

            }

            return await base.SendAsync(request, cancellationToken);
        }

    }
}




PollyPipelineHelper.cs

The following code makes it easier to use the ResiliencePipelineProvider that will look up configured resilience pipelines that are defined in the method AddNamedPollyPipelines further up in the article.



using Polly.Registry;

namespace HttpClientUsingPolly
{
    public static class PollyPipelineHelper
    {

        public static ValueTask<HttpResponseMessage> ExecuteWithPolicyAsync(
            this ResiliencePipelineProvider<string> pipelineProvider,
            string policyName,
            Func<CancellationToken, Task<HttpResponseMessage>> action,
            CancellationToken cancellationToken = default)
        {
            var pipeline = pipelineProvider.GetPipeline(policyName);

            return pipeline.ExecuteAsync(
                async ct => await action(ct),
                cancellationToken);
        }

    }
}





Program.cs

The folllowing setup is done in Program.cs, using the extension methods defined further up in the article. The setup below is the setup for the demo code, which is an Asp.net Web Api application. Minimal Api endpoints will be defined for the demo.



namespace HttpClientUsingPolly
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers();

            builder.Services.AddSwaggerGen();

            builder.Services.AddPollyHttpClient(); //set up polly v9 enabled http client (http client will be created via IHttpClientFactory)
            builder.Services.AddNamedPollyPipelines(); // set up polly v9 enabled resilience pipepline (http client will be created via ResiliencePipelineProvider 

            var app = builder.Build();

            // Configure the HTTP request pipeline.

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapGitHubUserEndpoints();

            app.MapControllers();

            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.Run();
        }
    }
}




Finally, the following Minimal Api endpoint methods first uses the resilience provider. As we can see, there is quite a bit of setup to make use of Polly using a resilience pipeline.

Minimal Api Endpoints using Polly enabled resilience pipeline

The following Github endpoints uses Polly resilience pipeline defined further up in the article. Both the IHttpClientFactory and ResiliencePipelineProvider are injected from DI container using the [FromService] attribute. There is some juggling to get to the httpClient to perform the request.

GithubEndpoints.cs



using Microsoft.AspNetCore.Mvc;
using Polly.Registry;
using System.Net.Http.Headers;
using System.Text.Json;

namespace HttpClientUsingPolly
{

    public static class GithubEndpoints
    {

        public const string HttpClientName = "GitHubClient";

        public static void MapGitHubUserEndpoints(this WebApplication app)
        {
            app.MapGet("/github-v1/{username}", async (
                string username,
                [FromServices] IHttpClientFactory httpClientFactory,
                [FromServices] ResiliencePipelineProvider<string> resiliencePipelineProvider) =>
            {
                string url = $"https://api.github.com/users/{username}";

                using var client = httpClientFactory.CreateClient(HttpClientName);
                client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));

                var response = await resiliencePipelineProvider.ExecuteWithPolicyAsync(
                    PollyExtensions.RetryResiliencePolicy,
                    ct => client.GetAsync(url, ct));

                response.EnsureSuccessStatusCode();
                var json = await response.Content.ReadAsStringAsync();
                var user = JsonSerializer.Deserialize<JsonElement>(json);

                return Results.Json(user);
            });

            app.MapGet("/test-retry-v1", async (
                [FromServices] IHttpClientFactory httpClientFactory,
                [FromServices] ResiliencePipelineProvider<string> resiliencePipelineProvider) =>
            {
                using var client = httpClientFactory.CreateClient(HttpClientName);

                var response = await resiliencePipelineProvider.ExecuteWithPolicyAsync(
                    PollyExtensions.RetryResiliencePolicy,
                    ct => client.GetAsync("https://example.com", ct));

                return Results.Json(response);
            });

        }

    }

}




Simpler use of Polly via IHttpClientFactory

The following Minimal API endpoints shows a simpler usage of Polly, where the use of Polly retry resilience strategy are implicit. It was defined in the method AddPollyHttpClient further up in this article.


    //use keyed httpclient and do not go via pipeline provider 

    app.MapGet("/github-v2/{username}", async (
       string username,
       [FromServices] IHttpClientFactory httpClientFactory) =>
    {
        string url = $"https://api.github.com/users/{username}";
        using var client = httpClientFactory.CreateClient(HttpClientName);

        var response = await client.GetAsync(url);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        var user = JsonSerializer.Deserialize<JsonElement>(json);
        return Results.Json(user);
    });

    app.MapGet("/test-retry-v2", async (
        [FromServices] IHttpClientFactory httpClientFactory) =>
    {
        using var client = httpClientFactory.CreateClient(HttpClientName);

        var response = await client.GetAsync("https://example.com");

        return Results.Json(response);
    });


As shown above, the Minimal API endpoints above uses IHttpClientFactory. The complexity of Polly is hidden and we can focus in the minimal API method to instantiate our HttpClient and make request and then retrieve the response content and return the result in the minimal API endpoint.

Further resources - Videos showing more information about Polly

Polly Resilience Strategy Video Resources
Title Description Length Link
Visual Studio Toolbox Live – Ensuring Resilience with Polly Martin Costello walks through Polly strategies like Retry, Circuit Breaker, Timeout, and how to use them in .NET apps. 59:29 Watch
Learn Live – Implement Resiliency in a Cloud-Native ASP.NET Core Microservice Deep dive into code-based and infrastructure-based resiliency using Polly and service meshes like Linkerd. 1:25:06 Watch
Implementing Resilience Strategies with Polly in ASP.NET Core Step-by-step guide to applying retry, circuit breaker, and timeout policies in ASP.NET Core using Polly. ~15 min Watch
Coding Short: Resilient .NET Apps with Polly Shawn Wildermuth gives a concise overview of Polly with practical examples of retry and circuit breaker usage. 14:07 Watch
Building Resilient Cloud Services with .NET 8 | .NET Conf 2023 Explores .NET 8 resiliency features and Polly integration in cloud-native applications. ~45 min Watch