Refit in .NET: Building Robust API Clients in C#
As a .NET developer, I've spent countless hours working with external APIs. It's a crucial part of modern software development, but let's be honest - it can be a real pain sometimes.
We've all been there, wrestling with HttpClient
, writing repetitive code, and hoping we didn't miss a parameter or header somewhere.
That's why I want to introduce you to Refit, a library that's been a game-changer for me.
Imagine turning your API into a live interface - sounds too good to be true, right? But that's exactly what Refit does. It handles all the HTTP heavy lifting, letting you focus on what matters: your application logic.
In this article, I'll explain how Refit can transform the way you work with APIs in your .NET projects.
What is Refit?
Refit is a type-safe REST library for .NET. It allows you to define your API as an interface, which Refit then implements for you. This approach reduces boilerplate code and makes your API calls more readable and maintainable.
You describe your API endpoints using method signatures and attributes, and Refit takes care of the rest.
Let me break down why I find Refit so powerful:
Automatic serialization and deserialization: You won't have to convert your objects to JSON and back. Refit handles all of that for you.
Strongly-typed API definitions: Refit helps you catch errors early. If you mistype a parameter or use the wrong data type, you'll know at compile time, not when your app crashes in production.
Support for various HTTP methods: GET, POST, PUT, PATCH, DELETE - Refit has you covered.
Request/response manipulations: You can add custom headers or handle specific content types in a straightforward way.
But what I appreciate most about Refit is how it promotes clean, readable code. Your API calls become self-documenting. Anyone reading your code can quickly understand what each method does without diving into implementation details.
Setting Up and Using Refit in Your Project
Let's set up Refit and see it in action using the JSONPlaceholder API. We'll implement a full CRUD interface and demonstrate its usage in a Minimal API application.
First, install the required NuGet packages:
Install-Package Refit
Install-Package Refit.HttpClientFactory
Now, let's create our Refit interface:
using Refit;
public interface IBlogApi
{
[Get("/posts/{id}")]
Task<Post> GetPostAsync(int id);
[Get("/posts")]
Task<List<Post>> GetPostsAsync();
[Post("/posts")]
Task<Post> CreatePostAsync([Body] Post post);
[Put("/posts/{id}")]
Task<Post> UpdatePostAsync(int id, [Body] Post post);
[Delete("/posts/{id}")]
Task DeletePostAsync(int id);
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public int UserId { get; set; }
}
We define our IBlogApi
interface with methods for all CRUD operations: GET (single and list), POST, PUT, and DELETE. The Post
class represents the structure of our blog posts.
Then you have to register Refit in your dependency injection container:
using Refit;
builder.Services
.AddRefitClient<IBlogApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
Finally, we can use IBlogApi
in our Minimal API endpoints:
app.MapGet("/posts/{id}", async (int id, IBlogApi api) =>
await api.GetPostAsync(id));
app.MapGet("/posts", async (IBlogApi api) =>
await api.GetPostsAsync());
app.MapPost("/posts", async ([FromBody] Post post, IBlogApi api) =>
await api.CreatePostAsync(post));
app.MapPut("/posts/{id}", async (int id, [FromBody] Post post, IBlogApi api) =>
await api.UpdatePostAsync(id, post));
app.MapDelete("/posts/{id}", async (int id, IBlogApi api) =>
await api.DeletePostAsync(id));
What I love about this setup is its simplicity. We've created a fully functional API that communicates with an external service, all in just a few lines of code. No manual HTTP requests, no raw JSON handling - Refit takes care of all that for us.
Query Parameters and Route Binding
When working with APIs, you often need to send data as part of the URL, either in the route or as query parameters. Refit makes this process simple and type-safe.
Let's extend our IBlogApi
interface with some more complex scenarios:
public interface IBlogApi
{
// Other methods omitted for brevity
[Get("/posts")]
Task<List<Post>> GetPostsAsync([Query] PostQueryParameters parameters);
[Get("/users/{userId}/posts")]
Task<List<Post>> GetUserPostsAsync(int userId);
}
public class PostQueryParameters
{
public int? UserId { get; set; }
public string? Title { get; set; }
}
Let's break this down:
GetPostsAsync
uses an object to represent query parameters. This approach is excellent for endpoints with many optional parameters. Refit will automatically convert this object into a query string.GetUserPostsAsync
demonstrates passing in route parameters (userId
) directly.
Using an object for query parameters makes your code type-safe and refactoring-friendly. If you need to add a new query parameter, you just add a property to PostQueryParameters
. Your existing code won't break, and your IDE can help you discover the new options.
Dynamic Headers and Authentication
Another common requirement when integrating with APIs is including custom headers or authentication tokens with your requests. Refit provides several ways to handle this, from simple static headers to dynamic, request-specific authentication.
Let's explore some scenarios:
public interface IBlogApi
{
[Headers("User-Agent: MyAwesomeApp/1.0")]
[Get("/posts")]
Task<List<Post>> GetPostsAsync();
[Get("/secure-posts")]
Task<List<Post>> GetSecurePostsAsync([Header("Authorization")] string bearerToken);
[Get("/user-posts")]
Task<List<Post>> GetUserPostsAsync([Authorize(scheme: "Bearer")] string token);
}
You can add a static header to all requests by using the
Headers
attributeWith the
Header
attribute, you can pass a header value dynamically as a parameterThe
Authorize
attribute is a convenient way to add Bearer token authentication
But what if you need to add the same dynamic header to all requests?
That's where DelegatingHandler
comes in handy.
You can learn more about using delegating handlers in this article.
I've found delegating handlers especially helpful in providing API keys, which are typically static.
JSON Serialization Options
Refit gives you flexibility when choosing and configuring your JSON serializer. By default, Refit uses System.Text.Json
, is the built-in JSON serializer in modern .NET versions.
However, you can easily switch to Newtonsoft.Json
if you need its features.
Here's how you can configure Refit to use it.
First, install the Newtonsoft.Json support package:
Install-Package Refit.Newtonsoft.Json
Then, configure Refit to use Newtonsoft.Json:
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Refit;
builder.Services.AddRefitClient<IBlogApi>(new RefitSettings
{
ContentSerializer = new NewtonsoftJsonContentSerializer(new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
})
})
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));
This setup uses camel case for property names and ignores null values when serializing.
System.Text.Json
is faster and uses less memory, making it a great default choice. However, Newtonsoft.Json
offers more features and might be necessary for compatibility with older systems or specific serialization needs.
Handling HTTP Responses
While Refit's default behavior of automatically deserializing responses into your defined types is convenient, there are times when you need more control over the HTTP response.
Refit provides two options for these scenarios: HttpResponseMessage
and ApiResponse<T>
.
Let's update the IBlogApi
to use these types:
public interface IBlogApi
{
[Get("/posts/{id}")]
Task<HttpResponseMessage> GetPostRawAsync(int id);
[Get("/posts/{id}")]
Task<ApiResponse<Post>> GetPostWithMetadataAsync(int id);
[Post("/posts")]
Task<ApiResponse<Post>> CreatePostAsync([Body] Post post);
}
Now, let's break down how to use these, starting with HttpResponseMessage
:
HttpResponseMessage response = await blogApi.GetPostRawAsync(1);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var post = JsonSerializer.Deserialize<Post>(content);
Console.WriteLine($"Retrieved post: {post.Title}");
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}
This approach gives you full control over the HTTP response. You can access status codes, headers, and the raw content. But you will have to deal with deserialization manually.
ApiResponse<T>
is a Refit-specific type that wraps the deserialized content and response metadata. It's a great middle ground when you need the typed response and access to headers or status codes.
Here's a more complex example using ApiResponse<T>
for creating a post:
var newPost = new Post { Title = "New Post", Body = "Content", UserId = 1 };
ApiResponse<Post> createResponse = await blogApi.CreatePostAsync(newPost);
if (createResponse.IsSuccessStatusCode)
{
var createdPost = createResponse.Content;
var locationHeader = createResponse.Headers.Location;
Console.WriteLine($"Created post with ID: {createdPost.Id}");
Console.WriteLine($"Location: {locationHeader}");
}
else
{
Console.WriteLine($"Error: {createResponse.Error.Content}");
Console.WriteLine($"Status: {createResponse.StatusCode}");
}
This approach allows you to access the created resource, check specific headers like Location
, and handle errors gracefully.
Takeaway
Refit transforms the way we interact with APIs in .NET applications. Converting your API into a strongly typed interface simplifies your code, enhances type safety, and improves maintainability.
The key Refit benefits we've explored include:
Simplified API calls with automatic serialization and deserialization
Flexible parameter handling for complex queries
Easy management of headers and authentication
Options for JSON serialization to fit your project's needs
Granular control over HTTP responses when required
In my experience, Refit shines in projects of all sizes. I've used it in small applications and large-scale microservices architectures. It eliminates boilerplate code, reduces the risk of errors, and allows you to focus on your application's core logic rather than the intricacies of HTTP communication.
Remember, while Refit makes API interactions more straightforward, it's not a substitute for understanding the underlying principles of RESTful communication and HTTP.
That's all for today. Stay awesome, and I'll see you next week.
P.S. You can find the source code for this example in this repository.
P.S. Whenever you’re ready, there are 3 ways I can help you:
Pragmatic Clean Architecture: Join 3,050+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
Modular Monolith Architecture: Join 950+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.
Subscribe to my newsletter
Read articles from Milan Jovanović directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Milan Jovanović
Milan Jovanović
I'm a seasoned software architect and Microsoft MVP for Developer Technologies. I talk about all things .NET and post new YouTube videos every week.