
Refit is a great little library for simplifying consumption of REST Web APIs, that even allows authentication via support for HTTP authorization header injection in Refit’s settings during set up.
In this article:
- Introduction to Refit
- Using Refit to Access REST Web APIs
- Authentication in Refit
- Some Hints and Tips When Using Refit
Introduction to Refit
Refit is an automatic type-safe REST library for .NET, supporting different framework flavours including .NET (Core), .NET Framework, and Xamarin. The official NuGet package details are here, along with some official documentation.
The library accelerates development activities that communicate with APIs by simplifying the work required to send requests and receive responses, including managing the creation and configuration of Web requests and deciphering responses. Refit automatically handles serialisation and deserialisation of data too.
Using Refit to Access REST Web APIs
A REST client can be developed very quickly, simply by defining any entity models and an interface (decorated with appropriate JSON and Refit attributes) and then instantiating a Refit client instance.
For example:
public class Customer
{
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("firstName")]
public string FirstName{ get; set; }
[JsonPropertyName("lastName")]
public string LastName{ get; set; }
[JsonPropertyName("birthDate")]
public DateTime DateOfBirth { get; set; }
}
public interface ICustomersApi
{
[Get("api/customers")]
Task<IEnumerable<Customer>> GetCustomersAsync();
[Get("api/customers/search?fn={firstName}&ln={lastName}")]
Task<IEnumerable<Customer>> GetCustomersAsync(string firstName, string lastName);
}Code language: C# (cs)
The above defines a Customer model class that describes the expected structure of the data, and an interface that describes the REST endpoint, complete with resource paths.
ASIDE: while it is fine to define interface methods like this, as Refit supports them, you may want to consider taking advantage of the richer method return types that Refit also supports. These are explained further on in this article in the Handling Responses from Refit section.
Initialising the Refit client can then be as simple as:
ICustomersApi client = RestService.For<ICustomersApi>("https://api.some-url.test");Code language: C# (cs)
Refit also provides a settings class that can be used to further configure clients. For example, the following code snippet informs Refit that URL parameters that are collections should be represented as comma separated set of values:
RefitSettings settings = new()
{
CollectionFormat = CollectionFormat.Csv
};
ICustomersApi client =
RestService.For<ICustomersApi>("https://api.some-url.test", settings);Code language: C# (cs)
Using the Refit client is equally simple:
public class CustomersReader()
{
private readonly ICustomersApi _client;
public CustomersReader()
{
_client = RestService.For<ICustomersApi>("https://api.some-url.test");
}
public async Task<IEnumerable<Customer>> GetAsync()
{
return await _client.GetCustomersAsync();
}
public async Task<IEnumerable<Customer>> SearchAsync(
string firstName, string lastName)
{
return await _client.GetCustomersAsync(firstName, lastName);
}
}Code language: C# (cs)
If you want to publish a Refit client to .NET’s service provider sub-system so it can be dependency injected into code then you could use RestService to create an instance of a Refit client and manually register that with the service provider. But, Refit provides a much easier approach if you add the additional Refit.HttpClientFactory package to your project.
Then you can register Refit client services like this:
builder.Services
.AddRefitClient<ICustomersApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.some-url.test"));Code language: C# (cs)
Authentication in Refit
While the above code allows Refit to access APIs with no authentication required, what about when the API has been secured and authentication and authorisation need to be done?
Refit provides a mechanism to support authentication via the RefitSettings class. Refit does not include code to manage authentication itself, and instead relies on the developer to perform any authentication outside the library’s classes and then supply the credentials (e.g. JWT bearer token) to Refit.
For example:
RefitSettings settings = new()
{
AuthorizationHeaderValueGetter = async (HttpRequestMessage request,
CancellationToken cancel)
{
// NOTE: for brevity, the code that instantiates the authenticator
// object is being omitted as not relevant to the example, and the
// code to authenticate would be identity provider specific anyway.
string token = await authenticator.GetJwtAsync();
if (request.Headers.Authorization is not null)
{
request.Headers.Authorization =
new AuthenticationHeaderValue(authHeader.Scheme, token);
}
return token;
}
};Code language: C# (cs)
If Refit.HttpClientFactory is being used, then a second option for configuring authentication exists. This is to register a HTTP message handler that always adds an Authorization header to outgoing requests. For example:
public class MyAuthHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
// Code to retrieve an auth token omitted for brevity.
string token = await GetJwtAsync();
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
builder.Services
.AddRefitClient<ICustomersApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.some-url.test"));
.AddHttpMessageHandler<MyAuthHandler>();Code language: C# (cs)
Some Hints and Tips When Using Refit
Server-Side Development – Don’t Use Client Interface Contracts
When developing both the client-side (REST client) and server-side (Web API) ends of a REST web service and using Refit, it can be tempting to define the Refit client interface contracts in a separate class library that can be referenced by both the client and server code projects to ensure client and server methods match exactly.
While at first you may think this is a wise move for consistency, there could be a few issues that arise by doing so.
Refit has its own set of attributes for decorating interface contracts and specifying REST resource paths (e.g. Get("api/some/path/to/resource")). The ASP.NET Core namespaces have their own set of attributes (e.g. Route("api/some/path/to/resource")) too, so they would have to be declared separately anyway and it makes sense to keep the two as separate definitions for that reason.
Also, using a client-side interface contract would only work server-side if Refit’s alternative return types aren’t being used (see the Handling Responses from Refit section below for more information about this) as the Web API would then be returning Refit specific class instances instead of the raw content the REST resources should.
A good compromise may be to define shared model classes in a separate library, and develop the Web API as usual, referencing the models class library. Then, define and develop the client interfaces in the code project where the client is going to be used (or possibly in a separate class library if clients are going to be used across multiple client projects) and re-use the same models class library again for consistency.
Server-Side Development – Better Error Messages
This isn’t really Refit specific but potentially adds value to exceptions raised by the server.
By default, if a server returns an error to a Refit client, then Refit will encapsulate that in a Refit.ApiException and throw that to the caller. Most likely the response status code that is returned by the server will be 500 (Internal Server Error) unless the server is explicitly tailoring responses to include appropriate status codes.
A simple way to implement improved error messaging in responses is by using custom exception types in conjunction with middleware that intercepts the response pipeline and handles any errors it detects more eloquently.
There are two obvious approaches to this:
- A set of per-status-code exception types. This has the potential to improve readability of the code but at the expense of the time taken to develop all the exception types.
- A single generic Web exception type that accepts a status code property. This is cleaner and less development work, but potentially at the expense of code readability.
Let’s look at both options.
For the exception class per status code implementation:
public sealed class BadRequestWebException : Exception
{
// Constructors omitted for brevity.
}
public sealed class NotFoundWebException : Exception
{
// Constructors omitted for brevity.
}
public sealed class ErrorResponseBody
{
public int StatusCode { get; set; } = 500;
public string Message { get; set; } = "Unspecified error";
}
public sealed class CustomWebExceptionInterceptorMiddleware
{
private readonly RequestDelegate _current;
public CustomWebExceptionInterceptorMiddleware(RequestDelegate current)
{
_current = current;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _current(context);
}
catch (BadRequestWebException badError)
{
await AddStatusCodeAndErrorMessageToResponseAsync(
context, HttpStatusCode.BadRequest, badError.Message);
}
catch (NotFoundWebException notFoundError)
{
await AddStatusCodeAndErrorMessageToResponseAsync(
context, HttpStatusCode.NotFound, notFoundError.Message);
}
// (Non-specific exception types are left to propagate.)
}
private static async Task AddStatusCodeAndErrorMessageToResponseAsync(
HttpContext context, HttpStatusCode statusCode, string errorMessage)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)statusCode;
var body = new()
{
StatusCode = (int)statusCode,
Message = errorMessage
};
await context.Response.WriteAsync(JsonSerializer.Serialize(body));
}
}Code language: C# (cs)
Conversely, for the generic Web exception scenario:
public sealed class WebApiException : Exception
{
// Some constructors omitted for brevity.
public WebApiException(int statusCode, string message, Exception inner)
: base(message, inner)
{
StatusCode = statusCode;
}
public int StatusCode { get; set; } = 500;
}
// ErrorResponseBody class as above would be here!
public sealed class CustomWebExceptionInterceptorMiddleware
{
private readonly RequestDelegate _current;
public CustomWebExceptionInterceptorMiddleware(RequestDelegate current)
{
_current = current;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _current(context);
}
catch (WebApiException error)
{
await AddStatusCodeAndErrorMessageToResponseAsync(
context, error.StatusCode, error.Message);
}
// (Non-specific exception types are left to propagate.)
}
// AddStatusCodeAndErrorMessageToResponseAsync method as above would be here!
}Code language: C# (cs)
In either case, registering the middleware with the Web Application is simple:
// In Program.cs...
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Code omitted for brevity (e.g. services registration).
WebApplication app = builder.Build();
// Code omitted for brevity (e.g. adding other middleware).
app.UseMiddleware<CustomWebExceptionInterceptorMiddleware>();
app.MapControllers();
app.Run();Code language: C# (cs)
In the Web API (Controller) code, the custom exception types can be thrown as normal:
public class CustomersApiController : ControllerBase
{
[HttpGet]
[Route("/api/customers/search")]
public async Task<IEnumerable<Customer>> SearchCustomersAsync(
string firstName, string lastName)
{
if (string.IsNullorEmpty(firstName) &&
string.IsNullOrEmpty(lastName))
{
throw new BadRequestWebException(
"A first name or last name must be supplied");
// OR, for the generic exception type:
// throw new WebApiException(
// HttpStatusCode.BadRequest,
// "A first name or last name must be supplied");
}
IEnumerable<Customer> customers = await GetAsync();
if (!customers.Any())
{
// Downstream functionality is probably offline,
// so return 'not found' instead of no results.
throw new NotFoundWebException("Not found.");
// OR, for the generic exception type:
// throw new WebApiException(HttpStatusCode.NotFound, "Not found.");
}
}
}Code language: C# (cs)
Using Custom Parameter Formatters
While most URL parameter formatting may work out-of-the-box there may be times when we want more control.
An obvious example of this relates to DateTime. We are accessing a remote Web API that expects a date-time parameter in ISO formatted universal time (aka Zulu time). Instead of simply hoping that a DateTime will be auto-magically converted into the right string format, we can be prescriptive and tell Refit exactly how we want it represented.
For example:
public class CustomUrlParameterFormatter : IUrlParameterFormatter
{
private readonly DefaultUrlParameterFormatter _defaultFormatter = new();
public string? Format(
object? value, ICustomAttributeProvider attributeProvider, Type type)
{
if (value is DateTime date)
{
return date.UniversalTime.ToString("yyyy-MM-ddTHH:mm:ssZ");
}
return _defaultFormatter.Format(value, attributeProvider, type);
}
}
RefitSettings settings = new()
{
UrlParameterFormatter = new CustomUrlParameterFormatter()
};Code language: C# (cs)
Here, we’ve defined a custom URL parameter formatter that performs custom formatting of DateTime objects, so they are always formatted as an ISO Zulu-time string. The custom formatter is assigned in the Refit settings object.
It is worth noting this custom formatter will now handle all URL parameter formatting, not just for those DateTime types, and that’s why we fall back to Refit’s default formatter for all other cases in the code above.
Handling Responses from Refit
In the code we’ve written so far, any propagated error that occurs within Refit would be caught and cause a Refit.ApiException instance to be raised instead. For example:
try
{
var results = await _client.GetAsync();
}
catch (Exception ex) // 'ex' would actually be an instance of Refit.ApiException.
{
// Log the error.
}Code language: C# (cs)
Refit will work just fine like this but if you aren’t aware of what’s happening then your error handling may miss out on important information like the status code that was returned.
Instead, if you had captured the ApiException type then it would be cast to the correct type and the additional members would be visible. For example:
try
{
var results = await _client.GetAsync();
}
catch (Refit.ApiException ex)
{
if (ex.StatusCode == HttpStatusCode.BadRequest)
{
throw new Exception(
$"Bad request returned when querying {ex.Uri}", ex);
}
if (ex.StatusCode == HttpStatusCode.Forbidden)
{
throw new Exception("You are not authorised to request customers data.", ex);
}
// In all other cases, raise a generally formatted exception.
throw new Exception(
$"Request failed with status code {ex.StatusCode} ({ex.ReasonPhrase}).",
ex);
}Code language: C# (cs)
There is also another custom exception type that Refit can throw in certain circumstances and that is ValidationApiException. This inherits from ApiException, but adds a ProblemDetails property that describes the validation problem(s) that occurred. This will only be thrown if the remote API implements RFC7807 and the response type was “application/problem+json”.
Aside: If you are using Serilog as your logging provider then you can enrich the logging mechanism by adding the Serilog.Exception.Refit package to your project as well. (Enrichment requires some configuration, so refer to the instructions on the NuGet package page for further information.)
Refit offers alternative method return type handling via IApiResponse, IApiResponse<T>, ApiResponse and ApiResponse<T>. These allow Refit to return much richer details about the response instead of just the data/content or throwing an exception.
When using these return types in your Refit methods, Refit will capture any errors and convert them into an ApiException object that will be written to the Error property of the object being returned. This changes the behaviour of the Refit client compared to when native return types are used, because you need to explicitly check if the request was successful and check the Error property if not.
For example:
public interface ICustomersApi
{
[Get("api/customers")]
Task<ApiResponse<IEnumerable<Customer>>> GetCustomersAsync();
[Get("api/customers/search?fn={firstName}&ln={lastName}")]
Task<ApiResponse<IEnumerable<Customer>>> GetCustomersAsync(
string firstName, string lastName);
}Code language: C# (cs)
When consuming the Refit client, the calling code needs to handle responses differently:
ICustomersApi client = RestService.For<ICustomersApi>("https://api.some-url.test");
ApiResponse<IEnumerable<Customer>> response = await client.GetCustomersAsync();
// At this point we won't know if the request was successful or not.
// So, need to check the outcome...
if (response.IsSuccessStatusCode)
{
return response.Content ?? new List<Customer>();
}
else if (response.Error is not null)
{
// Handle the ApiException however you want. For example:
throw new Exception("Request failed.", response.Error);
}
else
{
// The request failed but no exception was logged,
// so fall back on the status code and use our richer dataset
// for a more comprehensive description of the error too.
string content = "";
if (response.RequestMessage.Content is not null)
{
Stream reader = response.RequestMessage.Content.ReadAsStream();
int numBytes = (int)reader.Length;
if (numBytes > 0)
{
byte[] buffer = new byte[numBytes];
reader.Read(buffer, 0, numBytes);
content = System.Text.Encoding.UTF8.GetString(buffer);
}
}
throw new Exception("Request failed with status code " +
$"{response.StatusCode} ({response.ReasonPhrase}). " +
"Request details: " +
$"URI={response.RequestMessage.RequestUri}, " +
$"MessageType={response.RequestMessage.Method}, " +
$"Content={content}");
}Code language: C# (cs)
As you can see, using ApiResponse<T> gives us the potential to add a lot more detail into exception messages as we have access to the request details, the Refit settings, request and response headers, and other information too.
Whether you decide to use native return types and capture ApiException or use ApiResponse<T> may come down to personal choice. The ApiResponse<T> return type offers the potential to be able to log richer debugging data but at the expense of potentially more complicated code that may inhibit readability.
