Vaughan

Using Refit in ASP.NET Core to remove boilerplate code when consuming REST endpoints

.NET Core, Refit

I recently started using Refit in a project and its a really nice way to consume rest endpoints. Once you set it up, all you have to worry about is making a call against a interface and Refit will do all the work for you.

As an example, imagine that I have a person REST endpoint where I would like to query to get a list of people within a age range.


public interface IPersonApi
{
    [Get("/api/Person")]
    Task<ApiResponse<Person[]>> Search(PersonQuery search);
}

public class Person
{
    public string Name { get; set; }
}

public class PersonQuery
{
    public string Surname { get; set; }
    public int AgeFrom { get; set; }
    public int AgeTo { get; set; }
}

Wouldn't it be nice if that was all I had to worry about. I can call the Search method of my IPersonApi interface and await an array of people. Refit can make this happen!

I would like to make this call from a background worker service that just uses the interface and does something with the results.


public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IPersonApi personApi;

    public Worker(ILogger<Worker> logger, IPersonApi personApi)
    {
        _logger = logger;
        this.personApi = personApi;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var people = await personApi.Search(new PersonQuery { Surname = "Smith", AgeFrom = 13, AgeTo = 18 });

        //Do something
    }
}

The setup is quite simple, I just register it as follows:


class Program
{
    public static async Task Main(string[] args)
    {
        await CreateHostBuilder(args).RunConsoleAsync();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();

            services.AddOptions<OnePassOptions>().Bind(hostContext.Configuration.GetSection(nameof(OnePassOptions)));
            services.AddOptions<ApiOptions>().Bind(hostContext.Configuration.GetSection(nameof(ApiOptions)));
            services.AddTransient(sc => sc.GetRequiredService<IOptions<ApiOptions>>().Value);

            services
            .AddRefitClient<IPersonApi>()
            .ConfigureHttpClient((sp, c) =>
            {
                var options = sp.GetRequiredService<ApiOptions>();
                c.BaseAddress = options.Url;
            });

        });
}

public class ApiOptions
{
    public Uri Url { get; set; }
}

That just works. Really simple.

Just to make it a little closer to the real world, lets assume that this endpoint is using OAuth 2.0 which will only allow authenticated users to make requests.

To make this work without changing any of the business logic, you can implement your own DelegatingHandler which will intercept you Http request and add the relevant auth header.

public class ClientTokenHttpHandler : DelegatingHandler
{
    private readonly ILogger<ClientTokenHttpHandler> logger;
    private readonly IHttpClientFactory httpClientFactory;
    private readonly IMemoryCache cache;
    private readonly OnePassOptions _authOptions;
    private const string _clientKey = "ClientToken";

    public ClientTokenHttpHandler(ILogger<ClientTokenHttpHandler> logger, IOptions<OnePassOptions> authOptions, IHttpClientFactory httpClientFactory, IMemoryCache cache)
    {
        _authOptions = authOptions.Value;
        this.logger = logger;
        this.httpClientFactory = httpClientFactory;
        this.cache = cache;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!cache.TryGetValue<TokenResponse>(_clientKey, out var token) || token.ExpiresIn < 10)
        {
            token = await ClientTokensync();

            if (!token.IsError)
            {
                cache.Set(_clientKey, token);
            }
        }

        request.Headers.Add("Authorization", "Bearer " + token.AccessToken);
        return await base.SendAsync(request, cancellationToken);
    }

    private async Task<TokenResponse> ClientTokensync()
    {
        logger.LogInformation("Getting client token");

        var client = httpClientFactory.CreateClient();

        var discoveryResponse = await client.GetDiscoveryDocumentAsync(_authOptions.AuthorityUrl.ToString());

        if (discoveryResponse.IsError)
        {
            logger.LogError("Error retrieving DiscoveryDocument from {a}: {e}.", _authOptions.AuthorityUrl, discoveryResponse.Error);

            throw discoveryResponse.Exception ?? new Exception(discoveryResponse.Error);
        }

        var tokenResponse = await client.RequestTokenAsync(new TokenRequest
        {
            Address = discoveryResponse.TokenEndpoint,
            GrantType = _authOptions.GrantType,

            ClientId = _authOptions.ClientId,
            ClientSecret = _authOptions.ClientSecret,

            Parameters =
                {
                { "scope", _authOptions.Scope },
                }});

        if (tokenResponse.IsError)
        {
            logger.LogError("Error retrieving PasswordToken for Client {c} from {a}: {m}.",
                _authOptions.ClientId,
                _authOptions.AuthorityUrl,
                tokenResponse.Error);
        }

        return tokenResponse;
    }
}

public class OnePassOptions
{
    public Uri AuthorityUrl { get; set; }
    public string GrantType { get; set; }
    public string ClientId { get; set; }
    public string ClientSecret { get; set; }
    public string Scope { get; set; }
}

Then to register it you just add it as a HttpMessageHandler to the RefitClient.


    class Program
    {
        public static async Task Main(string[] args)
        {
            await CreateHostBuilder(args).RunConsoleAsync();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<Worker>();

                    services.AddOptions<OnePassOptions>().Bind(hostContext.Configuration.GetSection(nameof(OnePassOptions)));
                    services.AddOptions<ApiOptions>().Bind(hostContext.Configuration.GetSection(nameof(ApiOptions)));
                    services.AddTransient(sc => sc.GetRequiredService<IOptions<ApiOptions>>().Value);

                    services.AddMemoryCache();
                    services.AddTransient<ClientTokenHttpHandler>();

                    services
                         .AddRefitClient<IPersonApi>()
                          .ConfigureHttpClient((sp, c) =>
                          {
                              var options = sp.GetRequiredService<ApiOptions>();
                              c.BaseAddress = options.Url;
                          })
                         .AddHttpMessageHandler<ClientTokenHttpHandler>();

                });

    }

Your calls are now all authenticated!

Related Posts

BMC logoBuy me a coffee