Last Updated on September 29, 2023 by Aram
Refit is a REST library in .NET that allows you to handle and manage API calls in a clean way, It leverages the interfaces and attributes at its core that would translate to to RESTful APIs.
With many features including type-safe serializing and mapping for requests and responses, interceptors through delegating handlers, dynamic replacement, swift integration with the HttpClientFactory, simple usage of the interface with Dependency Injection.
Refit is inspired by one of the most popular libraries in Java/Android – Retrofit by Square.
If you are coming from a Java backend or native Android development background, you will surely know how Retrofit revolutionized the API integrations in these technologies.
With the brilliant concept of live interfaces and the simplicity of API building that was brought by Retrofit, henceforth, Refit was born.
With the rising popularity of Refit being inspired by Retrofit, now it is known as the Retrofit of .NET.
Being built on top of .NET Standard, Refit can be used within different technologies under .NET ecosystem.
In this tutorial, we will be using Refit in a windows desktop application to connect to RESTful APIs which we built previously using ASP.NET Core Web API 7
Clone this project from my GitHub Account.
We need to make sure it is being hosted locally side-by-side with the Windows Desktop application, so we can test our Refit implementation with the different endpoints provided by this RESTful API.
You can find the source code for this tutorial under my GitHub account as well.
So let’s start the tutorial to learn Refit in .NET
Create a new project and choose Windows Forms App:
Choose a path and name for the project, then choose .NET 7
Note: .NET 8 will be officially announced in the major Dotnet Conf 2023 event which is taking placing virtually from the 14th until the 16th of November, 2023.
There will be a huge announcements and updates for the whole .NET ecosystem and beyond, so stay tuned for the upcoming big event.
Now let’s get back to our tutorial.
Once you create the project, you should see a new form.
Leave the design part till the end.
Let’s start with building our requests and responses that will be used to communicate with the REST API using Refit
Requests
Create a new folder with name Requests, add the below classes:
LoginRequest
1 2 3 4 5 6 7 8 |
namespace TasksApp.Refit.DesktopClient.Requests { public class LoginRequest { public string Email { get; set; } public string Password { get; set; } } } |
TaskRequest
1 2 3 4 5 6 7 8 9 10 |
namespace TasksApp.Refit.DesktopClient.Requests { public class TaskRequest { public int Id { get; set; } public string Name { get; set; } public bool IsCompleted { get; set; } public DateTime Ts { get; set; } } } |
Responses
Now let’s create a folder for responses and include the below classes in it:
BaseResponse
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System.Text.Json.Serialization; namespace TasksApp.Refit.DesktopClient.Responses { public class BaseResponse { public bool Success { get; set; } [JsonPropertyName("errorCode")] public string ErrorCode { get; set; } [JsonPropertyName("error")] public string Error { get; set; } } } |
TaskResponse
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace TasksApp.Refit.DesktopClient.Responses { public class TaskResponse { public int Id { get; set; } public string Name { get; set; } public bool IsCompleted { get; set; } public DateTime Ts { get; set; } public override string? ToString() { return Name; } } } |
TaskResponseWrapper
1 2 3 4 5 6 7 8 9 |
using TasksApp.Refit.DesktopClient.Responses; namespace TasksApp.Refit.DesktopClient.Response { public class TaskResponseWrapper : BaseResponse { public List<TaskResponse> Tasks { get; set; } } } |
TokenResponse
1 2 3 4 5 6 7 8 9 10 |
namespace TasksApp.Refit.DesktopClient.Responses { public class TokenResponse : BaseResponse { public string AccessToken { get; set; } public string RefreshToken { get; set; } public int UserId { get; set; } public string FirstName { get; set; } } } |
Interfaces
One of the key and core features of refit, is the live interfaces.
Why is it live?
Because it allows the use of Refit Attributes to decorate each method to make it match the associated API endpoint.
So you will end up with a rich interface that binds to the API you are trying to connect with.
Refit’s ITasksApi interface
Let’s import the Nuget package for Refit and add the below interface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using TasksApp.Refit.DesktopClient.Requests; using TasksApp.Refit.DesktopClient.Responses; using Refit; namespace TasksApp.Refit.DesktopClient.Interfaces { public interface ITaskApi { [Post("/users/login")] Task<IApiResponse<TokenResponse>> Login([Body] LoginRequest loginRequest); [Post("/users/logout")] Task<IApiResponse> Logout(); [Get("/tasks")] Task<IApiResponse<List<TaskResponse>>> GetTasks(); [Post("/tasks")] Task<IApiResponse<TaskResponse>> AddTask([Body] TaskRequest task); [Delete("/tasks/{id}")] Task<IApiResponse<TaskResponse>> DeleteTask([AliasAs("id")] int id); } } |
As you can see it has different types of endpoints with concise attributes decorating each method.
To have a proper architecture that would adhere to the SOLID principles we will have interfaces as abstractions for the different functions or use cases that we are trying to cover in this tutorial.
So we will have an interface for users and another one for tasks
ITaskService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using TasksApp.Refit.DesktopClient.Requests; using TasksApp.Refit.DesktopClient.Response; using TasksApp.Refit.DesktopClient.Responses; using Refit; namespace TasksApp.Refit.DesktopClient.Interfaces { public interface ITaskService { Task<TaskResponseWrapper> GetTasks(); Task<TaskResponseWrapper> AddTask([Body] TaskRequest task); Task<BaseResponse> DeleteTask([Body] TaskRequest task); } } |
IUserService
1 2 3 4 5 6 7 8 9 10 |
using TasksApp.Refit.DesktopClient.Responses; namespace TasksApp.Refit.DesktopClient.Services { public interface IUserService { Task<TokenResponse> Login(string email, string password); Task Logout(); } } |
IAuthTokenStore
1 2 3 4 5 6 7 8 9 10 11 |
using TasksApp.Refit.DesktopClient.Responses; namespace TasksApp.Refit.DesktopClient.Interfaces { public interface IAuthTokenStore { TokenResponse? GetAuthToken(); void SetAuthToken(TokenResponse? tokenResponse); void ClearAuthToken(); } } |
Services
The services include the implementation for the interfaces
TaskService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Requests; using TasksApp.Refit.DesktopClient.Response; using TasksApp.Refit.DesktopClient.Responses; using Refit; namespace TasksApp.Refit.DesktopClient.Services { public class TaskService : ITaskService { public TaskService(ITaskApi taskApi) { TaskApi = taskApi; } public ITaskApi TaskApi { get; } public async Task<TaskResponseWrapper> AddTask([Body] TaskRequest task) { var addTaskResponse = await TaskApi.AddTask(task); if (!addTaskResponse.IsSuccessStatusCode) { if (addTaskResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized) { // Ideally this should call the refresh token endpoint to get a new access token // and refresh token and then retry the same action with the new token, and this should apply to all other endpoints // but for this tutorial, we are just expiring the session and redirecting to logout return new TaskResponseWrapper { Success = false, Error = "Session Expired", ErrorCode = "AT01" }; } return new TaskResponseWrapper { Success = false, Error = addTaskResponse.Error?.Content ?? "Invalid add task response", ErrorCode = "AT02" }; } if (addTaskResponse.Content is null) { return new TaskResponseWrapper { Success = false, Error = "Invalid add task response", ErrorCode = "AT02" }; } return new TaskResponseWrapper { Success = true, Tasks = new List<TaskResponse> { addTaskResponse.Content }}; } public async Task<BaseResponse> DeleteTask(TaskRequest task) { var deleteTaskResponse = await TaskApi.DeleteTask(task.Id); if (!deleteTaskResponse.IsSuccessStatusCode) { return new BaseResponse { Success = false, Error = deleteTaskResponse.Error?.Content ?? "Invalid delete task response", ErrorCode = "DT01" }; } return new BaseResponse { Success = true }; } public async Task<TaskResponseWrapper> GetTasks() { var getTasksResponse = await TaskApi.GetTasks(); if (!getTasksResponse.IsSuccessStatusCode) { return new TaskResponseWrapper { Success = false, Error = getTasksResponse.Error?.Content ?? "Invalid response", ErrorCode = "GT01" }; } return new TaskResponseWrapper { Success = true, Tasks = getTasksResponse.Content ?? new List<TaskResponse>()}; } } } |
UserService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using DesktopClient.Interfaces;using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Responses; namespace TasksApp.Refit.DesktopClient.Services { public class UserService : IUserService { public UserService(ITaskApi taskApi, IAuthTokenStore authTokenStore) { TaskApi = taskApi; AuthTokenStore = authTokenStore; } public ITaskApi TaskApi { get; } public IAuthTokenStore AuthTokenStore { get; } public async Task<TokenResponse> Login(string email, string password) { var response = await TaskApi.Login(new Requests.LoginRequest { Email = email, Password = password }); 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); return token; } public async Task Logout() { await TaskApi.Logout(); AuthTokenStore.ClearAuthToken(); } } } |
Stores
We will use an Authentication Store to manage the storage and retrieval of token .
To guarantee JWT token security, ideally the store should be connected to the Windows PasswordVault which is a safe place to store credentials , which includes the JWT
But for the sake of this tutorial, however, we will be caching the tokens in memory.
Add the below class to a new folder with name Stores:
AuthTokenStore
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Responses; namespace TasksApp.Refit.DesktopClient.Stores { public class AuthTokenStore : IAuthTokenStore { private TokenResponse? _token; public void ClearAuthToken() { _token = null; } public TokenResponse? GetAuthToken() => _token ?? new TokenResponse(); public void SetAuthToken(TokenResponse? tokenResponse) { _token = tokenResponse; } } } |
Handlers
This is for Refit to intercept the request prior to calling any endpoint, here we will include the authorization header as Bearer and pass the acces token
we will provide the access token from the Authentication store.
Add the below class to the Handlers folder:
AuthHeaderHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
using TasksApp.Refit.DesktopClient.Interfaces; using System.Net.Http.Headers; namespace TasksApp.Refit.DesktopClient.Handlers { 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); } } } |
Program.cs
Here are the different setup details for your program.cs
Note that you will need to import Nuget Packages for Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting
Also you will need the package Refit.HttpClientFactory to be able to use Retrofit’s Delegating Handler with the HttpClientFactory through DI.
Also take a note when you run the tasks api project, check the port number so you can configure it in the tasks app program.cs, as you can see below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Services; using TasksApp.Refit.DesktopClient.Stores; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Refit; using TasksApp.Refit.DesktopClient.Forms; using TasksApp.Refit.DesktopClient.Handlers; namespace TasksApp.Refit.DesktopClient { internal static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { // To customize application configuration such as set high DPI settings or default font, // see https://aka.ms/applicationconfiguration. ApplicationConfiguration.Initialize(); var host = CreateHostBuilder().Build(); Application.Run(host.Services.GetRequiredService<LoginForm>()); } static IHostBuilder CreateHostBuilder() { return Host.CreateDefaultBuilder() .ConfigureServices((context, services) => { services.AddTransient<ITaskService, TaskService>(); services.AddTransient<IUserService, UserService>(); services.AddSingleton<IAuthTokenStore, AuthTokenStore>(); services.AddTransient<AuthHeaderHandler>(); services.AddSingleton<LoginForm>(); services.AddSingleton<TasksForm>(); services.AddRefitClient<ITaskApi>() .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://localhost:7217/api")) .AddHttpMessageHandler<AuthHeaderHandler>(); }); } } } |
Forms
Here comes the UI design part, let’s start by designing our forms to make them ready for their code behind.
Create Folder with name Forms, and add the below forms to it:
Login Form
Here is the final design for the login screen
This is the code behind the login form:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Responses; using TasksApp.Refit.DesktopClient.Services; using System.Text.Json; namespace TasksApp.Refit.DesktopClient.Forms { public partial class LoginForm : Form { public LoginForm(IUserService userService, ITaskService taskService) { InitializeComponent(); UserService = userService; TaskService = taskService; } public IUserService UserService { get; } public ITaskService TaskService { get; } public TasksForm? TasksForm { get; set; } private async void btnLogin_Click(object sender, EventArgs e) { var email = txtEmail.Text.Trim(); var password = txtPassword.Text.Trim(); var tokenResponse = await UserService.Login(email, password); if (string.IsNullOrEmpty(tokenResponse.AccessToken)) { var baseResponse = JsonSerializer.Deserialize<BaseResponse>(tokenResponse.Error); lblMessage.Text = $"Unable to login due to {baseResponse?.Error ?? "technical error"}"; return; } TasksForm ??= new TasksForm(TaskService, UserService); TasksForm.LoginForm = this; TasksForm.Show(); Hide(); } } } |
Tasks Form
An this is how should the tasks form appear.
And here are the different functions that are bound to the tasks management events:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
using TasksApp.Refit.DesktopClient.Interfaces; using TasksApp.Refit.DesktopClient.Requests; using TasksApp.Refit.DesktopClient.Responses; using TasksApp.Refit.DesktopClient.Services; namespace TasksApp.Refit.DesktopClient.Forms { public partial class TasksForm : Form { public TasksForm(ITaskService taskService, IUserService userService) { InitializeComponent(); TaskService = taskService; UserService = userService; } public ITaskService TaskService { get; } public IUserService UserService { get; } public LoginForm? LoginForm { get; set; } private async void btnAddTask_Click(object sender, EventArgs e) { TaskRequest taskRequest = new() { Name = txtTask.Text.Trim(), IsCompleted = false, Ts = DateTime.Now }; var addResponseWrapper = await TaskService.AddTask(taskRequest); if (!addResponseWrapper.Success) { if (addResponseWrapper.ErrorCode == "AT01") { MessageBox.Show("Your login session has expired, must login again"); await Logout(); return; } MessageBox.Show($"Cannot add Task due to {addResponseWrapper.Error}"); return; } await RefreshTasks(); } private async void Tasks_Load(object sender, EventArgs e) { await RefreshTasks(); } private async Task<bool> RefreshTasks() { var tasksResponseWrapper = await TaskService.GetTasks(); if (!tasksResponseWrapper.Success) { MessageBox.Show($"Cannot load Tasks due to {tasksResponseWrapper.Error}"); return false; //return; } lstTasks.Items.Clear(); // return; tasksResponseWrapper.Tasks.ForEach(task => { if (lstTasks.Items.IndexOf(task) == -1) { lstTasks.Items.Add(task); } else { lstTasks.Items.Remove(task); } }); return true; } private async void btnDelete_Click(object sender, EventArgs e) { if (lstTasks.SelectedItem is not TaskResponse item) // Tip: This is a declaration pattern matching syntax { var message = "unable to select item"; MessageBox.Show(message); return; } var response = await TaskService.DeleteTask(new TaskRequest { Id = item.Id }); if (!response.Success) { MessageBox.Show($"Cannot delete Task due to {response.Error}"); return; } var tasksResponseWrapper = await TaskService.GetTasks(); if (!tasksResponseWrapper.Success) { MessageBox.Show($"Cannot load Tasks due to {tasksResponseWrapper.Error}"); return; } lstTasks.Items.Remove(item); } private async void btnLogout_Click(object sender, EventArgs e) { await Logout(); } private async Task<bool> Logout() { await UserService.Logout(); Hide(); LoginForm ??= new LoginForm(UserService, TaskService); LoginForm.TasksForm = this; LoginForm.Show(); return true; } } } |
Your final project folder structure should be like this:
Testing the App
Now that we have glued all the pieces together, time has come to test our work
First, as mentioned previously in this tutorial we need to have the TasksApi Project running on localhost
So head to the TasksApi project and let’s make sure it is running:
Now switch back to the tasks app, and hit F5
As a first open for the app, you should see the login screen:
to test the failing part, Enter wrong credentials
Now let’s test the passing part, once you press on Login, you will be redirected to the tasks dashboard screen:
Let’s try to add a task:
And delete a task
Now pressing logout, you will be redirected to the login screen
There you go.
You have learned how to build a very simple desktop application in .NET 7 that swiftly connects to some RESTful APIs, under localhost.
Exercises
Of course this is not the complete app, there are still some components to be enhanced and more functions to be added.
Here are some stuff that you can do to try yourself to complete its functions, improve on it and get your hands dirty with Refit and .NET.
You can integrate with more endpoints, like Update Task and Get User Profile, just follow the same pattern to bind the Refit interface with the new endpoints
You can include silent refresh for access tokens in the AuthHeaderHandler, where you can check the token expiry ahead of time and do a silent refresh, by calling the refresh token endpoint, before the current access token expires.
Currently , the TaksApi doesn’t return the Expiry time with the token response, you can work on including that as well and then in the Tasks App you can retrieve it and validate it.
Also, you might want to look into storing the token response in the PasswordVault,. currently is it being stored in memory, it is not the best experience since each time the app started it will ask the user for credentials to login.
From the PassordVault, you can have a better persistence while ensuring your JWT is securely stored away from the lurking villains!
Summary
In this tutorial we learned how to implement Refit in .NET, we built a very simple tasks management app using Windows Forms, 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 mapping to strongly-typed objects.
These are just a few of the amazing features of Refit, you can always explore further.
Make sure to do the above exercises to better understand how Refit is helping you build online services in a simple way.
Links to useful references in the below section.
References
Official Refit documentation on Github
Great read about Refit with Scott Hanselman
TasksApi Project Complete Tutorial
Learn .NET and C#
Here are some tutorials and articles from my blog to help your learn .NET and C#
Your Quick Guide to Pattern Matching in C#
12 Shorthand Operators in C# Every Developer Should Know About
Introduction to .NET Releases And Updates
13 Libraries in ASP.NET Core Every Developer Should Know About
A Quick Guide to Learn ASP.NET Core Web API
Collaborations
I am always open to discuss any protentional opportunities and collaborations.
Check this page to learn more about how we can benefit each other.
Bonus
Sharing with you this wonderful music from the baroque era, where you can lavish your ears with brilliant melodies with the complete 6 Brandenburg Concertos by J.S. Bach
Enjoy your time coding and listening.
Nice post!
Just to let any readers know that there’s another tool, built on top of Refit and called Apizr (www.apizr.net), aiming to provide built in management for retry, priority, auth, connectivity, data cache, data map and so on. Everything you said about Refit is still true with Apizr actually, but much more could be done the easy way and without the boilerplate. And yes I’m the owner of it but anyway, it could be good to know 🙂