1

I set up the following service collection with a Refit client and a Polly policy for dependency injection in my UWP app:

var serviceCollection = new ServiceCollection();

serviceCollection
    .AddRefitClient(typeof(IClassevivaAPI))
    .ConfigureHttpClient(
        (sp, client) =>
        {
            client.BaseAddress = new Uri(Endpoint.CurrentEndpoint);
        }
    )
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            .RetryAsync(1)
    );           

But I realized that Refit throws an ApiException when the http response status is not OK, so I looked up on StackOverflow (https://stackoverflow.com/a/74066105/9008381) and added the following policy to the services chain:

.AddPolicyHandler(Policy<IApiResponse>
.Handle<ApiException>()
.RetryAsync(2)

The problem is that when compiling the app on Visual Studio 2022 it gives the following error:

error CS1503: Argument 2: cannot convert from 'Polly.Retry.AsyncRetryPolicy<Refit.IApiResponse>' to 'Polly.IAsyncPolicy<System.Net.Http.HttpResponseMessage>'

Am I doing something wrong?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Gabboxl
  • 91
  • 3
  • 16

2 Answers2

1

If you want use Polly with Refit generated clients then you can do one of the followings:

Define Policy<string>

  1. Let's suppose your IClassevivaAPI looks like this:
public interface IClassevivaAPI
{
    [Get("/500")]
    Task<string> Get();
}
  1. and the refit client registration like that:
builder.Services
    .AddRefitClient(typeof(IClassevivaAPI))
    .ConfigureHttpClient((_, client) => client.BaseAddress = new Uri("https://httpstat.us/"));
  1. Use PolicyRegistry to register your retry policy
var registry = new PolicyRegistry();
registry.Add("A", Policy<string>
        .Handle<ApiException>(ex => { return true; }) //Predicate added to be able to add breakpoint here
        .RetryAsync(2, onRetry: (_, __) => { })); //onRetry added to be able to add breakpoint here
builder.Services.AddPolicyRegistry(registry);
  1. ExecuteAsync to decorate your api call
private readonly IClassevivaAPI _api;
private readonly IReadOnlyPolicyRegistry<string> _registry;

public WhateverController(IClassevivaAPI api, IReadOnlyPolicyRegistry<string> registry)
{
    (_api, _registry) = (api, registry);
}

[HttpGet]
public async Task Get()
{
    var retry = _registry.Get<IAsyncPolicy<string>>("A");
    var response = await retry.ExecuteAsync(_api.Get);        
}

Define Policy<IApiResponse>

If you wish to use IApiResponse in the policy definition then

registry.Add("B", Policy<IApiResponse>
        .Handle<ApiException>(ex => { return true; }) //Predicate added to be able to add breakpoint here
        .RetryAsync(2, onRetry: (_, __) => { })); //onRetry added to be able to add breakpoint here

and

var retry = _registry.Get<IAsyncPolicy<IApiResponse>>("B");
var response = await retry.ExecuteAsync(async () =>
{
    var result = await _api.Get();
    return new ApiResponse<string>(null, result, null);
});

Define Policy<HttpMessageResponse>

The AddPolicyHandler anticipates a policy which implements the IAsyncPolicy<HttpResponseMessage>. Neither A nor B implements it that's why you can't use the AddPolicyHandler for that.

But, if you change the interface like this:

public interface IClassevivaAPI
{
    [Get("/500")]
    Task<HttpResponseMessage> Get();
}

the policy needs to updated as well

builder.Services
    .AddRefitClient(typeof(IClassevivaAPI))
    .ConfigureHttpClient((_, client) => client.BaseAddress = new Uri("https://httpstat.us/"))
    .AddPolicyHandler(Policy<HttpResponseMessage>
        .HandleResult(res => res.StatusCode == HttpStatusCode.InternalServerError)
        .RetryAsync(2, onRetry: (_, __) => { })); //onRetry added to be able to add breakpoint here

and the usage

var result = await _api.Get();

Comparison of alternatives

Option Pros Cons
Policy<string> You can use high-level strongly typed response and ApiException You can't use it via AddPolicyHandler
Policy<IApiResponse> You can use low-level strongly typed response and ApiException You can't use it via AddPolicyHandler
Policy<HttpMessageResponse> You can use AddPolicyHandler You can't use most of refit's advantages
Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • Thank you for the detailed response. When I add the `AddPolicyRegistry(registry);` to the services chain I get the following error: `error CS1929: 'IHttpClientBuilder' does not contain a definition for 'AddPolicyRegistry' and the best extension method overload 'PollyServiceCollectionExtensions.AddPolicyRegistry(IServiceCollection, IPolicyRegistry)' requires a receiver of type 'Microsoft.Extensions.DependencyInjection.IServiceCollection'`. The purpose of what I am doing is to apply the policy to every request made with the Refit client. – Gabboxl Apr 03 '23 at 14:20
  • @Gabboxl On which object are you calling the `AddPolicyRegistry`? Based on the error message you try to call it after the `AddRefitClient` or `ConfigureHttpClient`. But it should be called on the `IServiceCollection` << in my example `builder.Services`. – Peter Csala Apr 03 '23 at 14:25
  • Alright thanks, it works. But I don't think this solves my main problem, which is to simplify the code. With your method, I would need to write every call for Refit for every endpoint. Isn't there a more straightforward way to apply the policy for Refit's ApiException globally similar to the method I used in my question? – Gabboxl Apr 03 '23 at 14:46
  • @Gabboxl Well, i have presented three ways how you can integrate polly and refit. I'm unaware of other alternative. – Peter Csala Apr 03 '23 at 14:56
1

I managed to solve the problem using a DispatchProxy class as Peter pointed out in his answer that Policy<IApiResponse> can't be used via AddPolicyHandler.

I created a DispatchProxy for my Refit client interface which executes my Refit API call through a custom Polly policy in the following way:

public class PoliciesDispatchProxy<T> : DispatchProxy
    where T : class, IClassevivaAPI
{
    private T Target { get; set; }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        var retryPolicy = Policy
            .Handle<AggregateException>()
            .RetryAsync(
                3,
                async (exception, retryCount, context) =>
                {
                    //we check whether the exception thrown is actually a Refit's ApiException
                    if (exception.InnerException is ApiException apiException)
                    {
                        if (apiException.StatusCode == System.Net.HttpStatusCode.Unauthorized)
                        {
                            //custom reauthentication code
                        }
                    }
                }
            );

        var fallback = Policy<object>
            .Handle<Exception>()
            .FallbackAsync(async ct =>
            {
                //if after the retries another exception occurs, then we let the call flow go ahead
                return targetMethod.Invoke(Target, args);
            });

        AsyncPolicyWrap<object> combinedpolicy = fallback.WrapAsync(retryPolicy);

        return combinedpolicy
            .ExecuteAsync(async () =>
            {
                var result = (targetMethod.Invoke(Target, args));

                if (result is Task task)
                {
                    task.Wait(); //we wait for the result of the task, so we retry the call if an exception is thrown
                }

                return result; //if no exception occur then we return the result of the method call
            })
            .Result;
    }

    public static T CreateProxy(T target)
    {
        var proxy = Create<T, PoliciesDispatchProxy<T>>() as PoliciesDispatchProxy<T>;
        proxy.Target = target;
        return proxy as T;
    }
}

In this case the policy retries the API call 3 times, if after the third time another exception is thrown the Fallback policy returns the call result.

I used it in the following way:

I get the Refit client using DI:

private readonly IClassevivaAPI apiClient;
App app = (App)App.Current;
apiClient = app.Container.GetService<IClassevivaAPI>();

Then I pass the client instance to the proxy class:

private readonly IClassevivaAPI apiWrapper;
apiWrapper = PoliciesDispatchProxy<IClassevivaAPI>.CreateProxy(apiClient);

Then I can make any API call from the apiWrapper without having to rewrite any existing code.


Be aware that when compiling with .NET Native (Release mode) using reflections it will cause the app to crash, so for this case you will need to add the following assembly tags for Microsoft extensions to the Default.rd.xml file:

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>

    <Assembly Name="*Application*" Dynamic="Required All" />
    
    <Assembly Dynamic="Required All" Name="Microsoft.Extensions.Options"/>
    <Assembly Dynamic="Required All" Name="Microsoft.Extensions.Logging"/>
    <Assembly Dynamic="Required All" Name="Microsoft.Extensions.Http"/>
  </Application>
</Directives>
Gabboxl
  • 91
  • 3
  • 16