Polly's retry policy performs the exact same operation whenever it triggers. So, by the default you can't alter the request.
But you can modify it in the onRetryAsync
delegate, which is fired before the actual retry happens.
In order to demonstrate this I will use the WireMock.NET library to mimic your downstream system.
So, first let's create a webserver, which listens on the 40000
port on the localhost
at the /api
route:
protected const string route = "/api";
protected const int port = 40_000;
protected static readonly WireMockServer server = WireMockServer.Start(port);
protected static readonly IRequestBuilder endpointSetup = Request.Create().WithPath(route).UsingPost();
Then setup a scenario to simulate that the first request fails with 400 and then the second succeeds with 200.
protected const string scenario = "polly-retry-test";
server.Reset();
server
.Given(endpointSetup)
.InScenario(scenario)
.WillSetStateTo(1)
.WithTitle("Failed Request")
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.BadRequest));
server
.Given(endpointSetup)
.InScenario(scenario)
.WhenStateIs(1)
.WithTitle("Succeeded Request")
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
Let's test it
protected static readonly HttpClient client = new HttpClient();
var result = await client.PostAsync($"http://localhost:{port}{route}", new StringContent(""));
Console.WriteLine(result.StatusCode);
result = await client.PostAsync($"http://localhost:{port}{route}", new StringContent(""));
Console.WriteLine(result.StatusCode);
the output will be the following:
BadRequest
OK
Okay, so now we have a downstream simulator, now it's time to focus on the retry policy. For the sake of simplicity I'll serialize a List<int>
collection to post it as the payload.
I setup the retry whenever it receives a 400 then in its onRetryAsync
delegate I examine the response and remove the unwanted integers.
AsyncRetryPolicy<HttpResponseMessage> retryInCaseOfPartialSuccess = Policy
.HandleResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.BadRequest)
.RetryAsync(1, onRetryAsync: (dr, _, __) => {
//TODO: Process response from: `dr.Result.Content`
//TODO: Replace the removal logic to appropriate one
dtoIds.RemoveAt(0);
return Task.CompletedTask;
});
Let's call the downstream API with the decorated retry policy:
await retryInCaseOfPartialSuccess.ExecuteAsync(async (_) => {
var payload = JsonSerializer.Serialize(dtoIds);
Console.WriteLine(payload); //Only for debugging purposes
return await client.PostAsync($"http://localhost:{port}{route}", new StringContent(payload));
}, CancellationToken.None);
Let's put all this together:
protected const string scenario = "polly-retry-test";
protected const string route = "/api";
protected const int port = 40_000;
protected static readonly WireMockServer server = WireMockServer.Start(port);
protected static readonly IRequestBuilder endpointSetup = Request.Create().WithPath(route).UsingPost();
protected static readonly HttpClient client = new HttpClient();
private static async Task Main()
{
server.Reset();
server
.Given(endpointSetup)
.InScenario(scenario)
.WillSetStateTo(1)
.WithTitle("Failed Request")
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.BadRequest));
server
.Given(endpointSetup)
.InScenario(scenario)
.WhenStateIs(1)
.WithTitle("Succeeded Request")
.RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK));
//var result = await client.PostAsync($"http://localhost:{port}{route}", new StringContent(""));
//Console.WriteLine(result.StatusCode);
//result = await client.PostAsync($"http://localhost:{port}{route}", new StringContent(""));
//Console.WriteLine(result.StatusCode);
await IssueRequestAgainstDownstream(new List<int> { 1, 2 });
}
private static async Task IssueRequestAgainstDownstream(List<int> dtoIds)
{
AsyncRetryPolicy<HttpResponseMessage> retryInCaseOfPartialSuccess = Policy
.HandleResult<HttpResponseMessage>(response => response.StatusCode == HttpStatusCode.BadRequest)
.RetryAsync(1, onRetryAsync: (dr, _, __) => {
//TODO: Process response from: `dr.Result.Content`
//TODO: Replace the removal logic to appropriate one
dtoIds.RemoveAt(0);
return Task.CompletedTask;
});
await retryInCaseOfPartialSuccess.ExecuteAsync(async (_) => {
var payload = JsonSerializer.Serialize(dtoIds);
Console.WriteLine(payload); //Only for debugging purposes
return await client.PostAsync($"http://localhost:{port}{route}", new StringContent(payload));
}, CancellationToken.None);
}
So, what we have done?
- Created a downstream mock to simulate 400 and 200 subsequent responses
- Created a retry policy which can amend the request's payload if it receives 400
- Put the serialization logic and the http call inside the retry, so, that we can always serialize the most recent list of objects