Get to know Refit - an automatic Rest Api library for .NET

We all have had the situation when we had to make external API calls at some point. We then had to write a certain part of logic to handle all the code involving HttpClient, requests, responses, serialization/deserialization, etc. All this process risks gathering errors, by using the wrong parameter or a header by mistake. If all this were available through an interface, that would make the process much easier, right? That's precisely what Refit is doing; it takes care of all this integration, letting developers focus on the main application logic.

In this article, we'll get to know Refit better, what it does, certain particularities of it within a .NET with Blazor application, and a functional application that does monthly planning and requires authentication.


Author: Simona Balan, Software Developer

Simona, Software Developer

Refit is the type-safe .NET library for .Net, .Net Core, and Xamarin frameworks. It’s being built on the standard of .NET, and can be used with different technologies under the .NET ecosystem.

It directs you to define your API as an interface and also uses attributes, parameters, and headers that translate to the RESTFul Api.

Refit is valuable because it provides:

  • Automatic serialization and deserialization: Refit handles request serialization and response deserialization automatically. (by using System.Text.Json)
  • Type Safety: Strongly typed (query) parameters and URL strings help you prevent errors at runtime.
  • Offers support for various HTTP attributes: GET, POST, PUT, PATCH, OPTIONS
  • Simplifies Testing: Mocking the interface helps with unit tests.
  • Error handling: Centralized, standardized

Setting up refit and usage in your project

As a case study for this article, we'll build an application using Blazor WASM and Refit, building a clear interface for JWT authentication and planning activities in the calendar (for an authenticated user).

We’ll create a .NET 8 Blazor Web Assembly application and install the following packages:

Install-Package Refit
Install-Package Refit.HttpClientFactory

The Refit interface that will be used will look like this:

using Refit;
public interface ITaskApi
{
    [Post("/users/login")]
    Task<IApiResponse<TokenResponse>> Login([Body] LoginRequest loginRequest);
    [Post("/users/signup")]
    Task<IApiResponse<SignupResponse>> Signup([Body] SignupRequest loginRequest);
    [Post("/users/logout")]
    Task<IApiResponse> Logout();
    [Get("/Activity")]
    Task<IApiResponse<List<ActivityResponse>>> GetTasks();
    [Post("/Activity")]
    Task<IApiResponse<ActivityResponse>> AddTask([Body] ActivityRequest task);
    [Put("/Activity")]
    Task<IApiResponse<ActivityResponse>> UpdateTask([Body] ActivityRequest task);
    [Delete("/Activity/{id}")]
    Task<IApiResponse<ActivityResponse>> DeleteTask([AliasAs("id")] int id);
}

We define our ITaskApi for authentication methods of a user (login, signup and logout) and CRUD methods for an Activity in the Calendar, using Http attributes: GET, POST, PUT, DELETE.

We register Refit into the dependency injection container, as follows:

builder.Services.AddRefitClient<ITaskApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri
("https://localhost:7250/api"))
        .AddTransientHttpErrorPolicy(b => b.WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds
(Math.Pow(2, retryAttempt))
        ))
        .AddHttpMessageHandler<AuthHeaderHandler>();

As an additional note on this library, we use AliasAs attribute to specify a custom name for a property when the parameter name is different from the one used in the URL path, like this:

[Delete("/Activity/{id}")]
Task<IApiResponse<ActivityResponse>> DeleteTask([AliasAs("id")] int activityId);

We are also able to use Polly to specify a mechanism of retry related to HttpClient.

Authentication with headers/message handlers

Authentication can be set (static or dynamic) by using headers applied in various places: on the interface itself or as an attribute on the method parameter, like this:

public interface ITaskApi 
{
      [Headers("User-Agent: MyAwesomeApp/1.0")]
      [Get("/Activity")]
      Task<IApiResponse<List<ActivityResponse>>> GetActivityAsync();
     [Get("/Activity")]
     Task<List<Activity>> GetActivityAsync([Header("Authorization")] 
string bearerToken);
     [Get("/Activity")]
     Task<List<Activity>> GetActivityAsync([Authorize(scheme: "Bearer")] 
string token);
}

Or by using Delegating Handler we can set the Authorization header, before sending any request (in which user sets the jwt token received from ITaskApi:

public class AuthHeaderHandler : DelegatingHandler
{
    private readonly IAuthTokenStore authTokenStore;
    public AuthHeaderHandler(IAuthTokenStore authTokenStore)
    {
        this.authTokenStore = authTokenStore;
    }
    protected override async Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = authTokenStore.GetAuthToken();
        if (token is null)
        {
           return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }
        //TODO: Add code here to refresh the token if it has expired or near expiry 
(silent refresh)
        request.Headers.Authorization = new AuthenticationHeaderValue
("Bearer", token.AccessToken);
        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

Being injected like this:

builder.Services.AddRefitClient<ITaskApi>()
    .AddHttpMessageHandler<AuthHeaderHandler>();

JSON serialization options

By default, Refit uses System.Text.Json for serialization, with the possibility to change it with Newtonsoft.Json, manually imported (Refit.Newtonsoft.Json package), if a user wants to use it specifically.

Handling HTTP response

Refit provides the generic type ApiResponse<T>[PA, which wraps the deserialized content and response metadata and can be used as return type. It is a space where you need the typed response and access to headers or status codes, reason phrase (ex, 404 Not Found) or response version.

We also use Blazored.LocalStorage package, with ILocalStorageService class for storing the jwt token.

public async Task<TokenResponse> LoginAsync(LoginRequest loginModel)
{
    var response = await TaskApi.Login(loginModel);
 
    if (!response.IsSuccessStatusCode)
    {
        return new TokenResponse { Success = false, Error = response.Error?.Content ?? 
"Invalid response", ErrorCode = "L01" };
    }
 
    var token = response.Content;
 
    if (token is null)
    {
        return new TokenResponse { Success = false, Error = "Token is null", 
ErrorCode = "L02" };
    }
 
    AuthTokenStore.SetAuthToken(token);
    await LocalStorage.SetItemAsync("authToken", token.AccessToken);
 ((ApiAuthenticationStateProvider)_authenticationStateProvider).
MarkUserAsAuthenticated(loginModel

Here are the 2 services that were used :

public class AuthService : IAuthService
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    public AuthService(ITaskApi taskApi, IAuthTokenStore authTokenStore, 
ILocalStorageService localStorage, AuthenticationStateProvider authenticationStateProvider)
    {   TaskApi = taskApi;
        AuthTokenStore = authTokenStore;
        LocalStorage = localStorage;
        _authenticationStateProvider = authenticationStateProvider;
    }
    public ITaskApi TaskApi { get; }
    public IAuthTokenStore AuthTokenStore { get; }
    public ILocalStorageService LocalStorage { get; }
    public async Task<SignupResponse> RegisterAsync(SignupRequest registerModel)
    {
        var response = await TaskApi.Signup(registerModel);
        if (!response.IsSuccessStatusCode)
        {
            var options = new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            };
            var responseError = JsonSerializer.Deserialize<SignupResponse>
(response.Error?.Content, options);
            return new SignupResponse { Success = false, 
Error = responseError?.Error ?? 
"Invalid response", ErrorCode = responseError?.ErrorCode ?? "S01" };
        }
        var emailUser = response.Content;
        if (emailUser == null)
        {
            return new SignupResponse { Success = false, Error = "Invalid response" };
        }
        return emailUser;
    }
    public async Task<TokenResponse> LoginAsync(LoginRequest loginModel)
    {
        var response = await TaskApi.Login(loginModel);
if (token is null)
        {
            return new TokenResponse { Success = false, Error = "Token is null", 
ErrorCode = "L02" };
        }
        AuthTokenStore.SetAuthToken(token);
        await LocalStorage.SetItemAsync("authToken", token.AccessToken);
        ((ApiAuthenticationStateProvider)_authenticationStateProvider).
MarkUserAsAuthenticated(loginModel.Email);
        return token;
    }
    public async Task LogoutAsync()
    {
        await TaskApi.Logout();
        AuthTokenStore.ClearAuthToken(); 
        await LocalStorage.RemoveItemAsync("authToken");             
((ApiAuthenticationStateProvider)_authenticationStateProvider).MarkUserAsLoggedOut();
    }
}

And ActivityService.cs class :

public class ActivityService : IActivityService
{
    private readonly AuthenticationStateProvider _authenticationStateProvider;
    public ActivityService(ITaskApi taskApi, IAuthTokenStore authTokenStore,
        ILocalStorageService localStorage, AuthenticationStateProvider 
authenticationStateProvider)
    {
        TaskApi = taskApi;
        AuthTokenStore = authTokenStore;
        LocalStorage = localStorage;
        _authenticationStateProvider = authenticationStateProvider;
    }
    public ITaskApi TaskApi { get; }
    public IAuthTokenStore AuthTokenStore { get; }
    public ILocalStorageService LocalStorage { get; }
    public async Task<GetActivitiesResponse> GetActivitiesAsync()
    {
        var response = await TaskApi.GetTasks();
        if (!response.IsSuccessStatusCode)
        {
            return new GetActivitiesResponse 
            { 
                Success = false, 
                Error = response.Error?.Content ?? "Invalid response", 
                ErrorCode = response.Error.StatusCode.ToString() 
            };
        }
        var activities = response.Content;
        return new GetActivitiesResponse { 
            Activities = activities.Select(a => new Models.Activity 
            { 
                Description = a.Description, 
                Id=a.Id, 
                IsCompleted=a.IsCompleted, 
                PlannedDate=a.PlannedDate, 
                Title=a.Title
{        
       if (updateActivityResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
        {
            return new ActivityResponseWrapper 
            { 
                Success = false, 
                Error = "Session Expired", 
                ErrorCode = "UA01" 
            };
        }
        return new ActivityResponseWrapper 
        { 
            Success = false, 
            Error = updateActivityResponse.Error?.Content ?? 
"Invalid update task response", 
            ErrorCode = "UA02" 
        };
    }
    if (updateActivityResponse.Content is null)
    {
        return new ActivityResponseWrapper 
        { 
            Success = false, 
            Error = "Invalid update task response", 
            ErrorCode = "UA02" 
        };
    }
    return new ActivityResponseWrapper 
    { 
        Success = true, 
        Activity = new List<ActivityResponse> { updateActivityResponse.Content } 
    };
}
public async Task<ActivityResponseWrapper> AddActivityAsync([Body] ActivityRequest 
Error = "Invalid add activity response", 
                ErrorCode = "AA02" 
            };
        }
        return new ActivityResponseWrapper 
        { 
            Success = true, 
            Activity = new List<ActivityResponse> { addTaskResponse.Content } 
        };
    }
    public async Task<BaseResponse> DeleteActivityAsync(int activityId)
    {
        var deleteTaskResponse = await TaskApi.DeleteTask(activityId);
        if (!deleteTaskResponse.IsSuccessStatusCode)
        {
            return new BaseResponse 
            { 
                Success = false, 
                Error = deleteTaskResponse.Error?.Content ?? 
"Invalid delete activity response", 
                ErrorCode = "DA01" 
            };
        }
        return new BaseResponse { Success = true };
    }
}

And, of course, some pictures from planning within the app:

Login screen
Login screen
Planning screen
Planning screen
Edit Activity screen
Edit Activity screen
Add Activity screen
Add Activity screen

Note: For this project to work, there was also developed the Api on which it makes calls to and that is found under the following url: https://github.com/SimonaBalan/CalendarPlannerApi. This PlannerActivitiesApi should also be in running mode and includes Swagger UI for easy exploration of the REST API endpoints and to automatically generate API documentation. It's available by accessing the /swagger/index.html as in the next image:

Swagger page for the CalendarApi
Swagger page for the CalendarApi

Summary

In this tutorial we learned how to implement Refit in .NET, we built a very simple planner management app, with jwt authentication, using Blazor, and connected to an existing RESTful APIs built using ASP.NET Core Web API Technology.

We learned how Refit can simplify connecting the client to REST APIs, with the different features like live interfaces and attributes, integration with HttpClientFactory, interceptors through delegating handlers, type-safe auto serializing and map to strongly-typed objects.

These are just a few of the amazing features of Refit, you can always explore further.

Furthermore, as next step, this app can be deployed to the Cloud, for ex in Azure as a static web App or an AppService, enhancing scalability, security and manageability.

Bibliography:

https://dotnet.rest/docs/libraries/client/refit/

https://github.com/reactiveui/refit

https://www.hanselman.com/blog/exploring-refit-an-automatic-typesafe-rest-library-for-net-standard

https://www.telerik.com/blogs/aspnet-core-basics-simplifying-api-integration-refit

https://dotnet.rest/docs/libraries/client/refit/