Usually when consuming an HTTP service, you will want to use a typed HTTP client. There are two approaches on how to write them:
You can generate it using an existing OpenAPI documentation and your favorite tool that generates the actual code. Unfortunately, not all services provide an OpenAPI documentation and even if they did, the generated code is not always exactly what you need. Maybe there are arrays in the query string that need to be parsed differently or maybe there is some other inconstancy that forces you to make changes to the generated code and persist them after regeneration. Additionally, many of the generation tools don’t use any boilerplate code so you’ll have to change every single method call which defeats the purpose of the auto-generation. Another reason to not use auto-generated HTTP clients is that usually they will generate code for the whole service and maybe you want just a tiny part of it. This includes both methods and resources.
Another way that lets you have more control over the client is to just write it manually. The problem with this is that the process is tedious at best and error-prone at least. To avoid this, we've created an extensible typed HTTP client base class that provided a lean contract of the service can execute HTTP requests and process their responses.
Before being able to consume a service you have to define its contract or at least the parts of it you'll be using. The contract is an interface with async methods that are decorated with the HttpEndpointAttribute
. The typed HTTP client will create an implementation of the contract on its initialization during runtime. The implementation details will be inferred from the methods' signatures and their attributes. Below is an example of a service contract. You have to manually create the interface but the resources' types can be either manually created or auto-generated by a tool when applicable.
public class Book
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Author { get; set; }
public DateTime PublicationDate { get; set; }
}
public interface ILibraryService
{
[HttpEndpoint("GET", "/books/{id}")]
Task<Book> GetBookAsync(string id);
[HttpEndpoint("GET", "/books")]
Task<IEnumerable<Book>> GetBooksByAuthorAsync(string author);
[HttpEndpoint("POST", "/books")]
Task<Book> AddBookAsync([HttpBody] Book book);
[HttpEndpoint("DELETE", "/books/{id}")]
Task DeleteBookAsync(string id);
}
You can access an instance of the service contract in two different ways:
Registering the service contract to the IServiceCollection
comes with the benefit of leveraging the IHttpClientFactory
. For more info on why you should use it, check out this article.
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(ILibraryService services)
{
services.AddTypedHttpClient<ILibraryService, JsonHttpClient<ILibraryService>>(client =>
{
client.BaseAddress = new Uri("https://mylibraryservice.com");
});
// Remaining code deleted for brevity.
After that you'll be able to inject ILibraryService
into your code and it will send HTTP requests to https://mylibraryservice.com
and parse the responses properly. You can also use the parameter of the AddTypedHttpClient
method to configure the underlying HttpClient
according to your needs.
public class InjectedClass
{
private readonly ILibraryService _libraryService;
public InjectedClass(ILibraryService libraryService)
{
_libraryService = libraryService;
}
async void ExampleMethodAsync(string bookId)
{
Book book = await _libraryService.GetBookAsync(bookId);
// ...
}
}
You can always just initialize a new typed HTTP client via its constructor and then access its Endpoints
property but you have to be careful with the lifecycle of the HttpClient
.
async void ExampleMethodAsync(string bookId)
{
using HttpClient httpClient = new HttpClient()
{
BaseAddress = new Uri("https://mylibraryservice.com")
};
JsonHttpClient<ILibraryService> service = new(httpClient);
Book book = await service.Endpoints.GetBookAsync(bookId);
// ...
}
Currently the only available client is the JsonHttpClient
. It works by serializing and deserializing message contents by using System.Text.Json.JsonSerializer
.
Additionally, the TypedHttpClient
base class provides useful abstract and virtual methods for implementing your own typed HTTP clients.
Moreover, if you are in need of another commonly used type of client, don't hesitate to create a feature request and we'll be happy to implement it for you.