14 KiB
sidebar_position | title |
---|---|
3 | Make HTTP requests |
Data format
Two operations exist to retrieve data from an API, the first is to retrieve raw data in JSON format, the second is to use serialization / deserialization.
In our case, the library used implements serialization / deserialization by default.
In order to be able to manipulate our data we will use our Item
component class locally, the data received comes from the serialization of the Item
class of the server.
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public string ImageBase64 { get; set; }
}
:::info Adding or deleting fields in our model does not generate an error when retrieving data. :::
Using the IOC
Thanks to our IOC we will therefore call our API in a specific service implementing the IDataService
interface.
Create the DataApiService
class:
public class DataApiService : IDataService
{
private readonly HttpClient _http;
public DataApiService(
HttpClient http)
{
_http = http;
}
public async Task Add(ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
// Save the data
await _http.PostAsJsonAsync("https://localhost:7234/api/Crafting/", item);
}
public async Task<int> Count()
{
return await _http.GetFromJsonAsync<int>("https://localhost:7234/api/Crafting/count");
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
return await _http.GetFromJsonAsync<List<Item>>($"https://localhost:7234/api/Crafting/?currentPage={currentPage}&pageSize={pageSize}");
}
public async Task<Item> GetById(int id)
{
return await _http.GetFromJsonAsync<Item>($"https://localhost:7234/api/Crafting/{id}");
}
public async Task Update(int id, ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
await _http.PutAsJsonAsync($"https://localhost:7234/api/Crafting/{id}", item);
}
public async Task Delete(int id)
{
await _http.DeleteAsync($"https://localhost:7234/api/Crafting/{id}");
}
public async Task<List<CraftingRecipe>> GetRecipes()
{
return await _http.GetFromJsonAsync<List<CraftingRecipe>>("https://localhost:7234/api/Crafting/recipe");
}
}
We now have a class allowing us to pass all of our calls through an API.
Register the data service
Open the Program.cs
file and edit the following line:
...
builder.Services.AddScoped<IDataService, DataApiService>();
...
Add the API sample to your project
Download this project.
Unzip the file in the directory of your project, at the same place of the directory of the Blazor Project.
Example:
On Visual Studio, click right on the solution and choose Add => Existing Project...
Select the file Minecraft.Crafting.Api.csproj
in the directory Minecraft.Crafting.Api
.
Your solution now contains two projects, your client and the sample API.
How to start two projects at same time in Visual Studio
For test your client with the API, you have to start the two projects at same time.
For this, on Visual Studio, click right on the solution and choose Properties
On the new screen select "Multiple startup projects" and select "Start" for the two projects.
When you start debug mode, the two projects are started.
Concept: IHttpClientFactory
An IHttpClientFactory
can be registered and used to configure and create HttpClient instances in an app.
IHttpClientFactory
offers the following benefits:
- Provides a central location for naming and configuring logical
HttpClient
instances. For example, a client named github could be registered and configured to access GitHub. A default client can be registered for general access. - Codifies the concept of outgoing middleware via delegating handlers in
HttpClient
. Provides extensions for Polly-based middleware to take advantage of delegating handlers inHttpClient
. - Manages the pooling and lifetime of underlying
HttpClientMessageHandler
instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managingHttpClient
lifetimes. - Adds a configurable logging experience (via
ILogger
) for all requests sent through clients created by the factory.
Consumption patterns
There are several ways IHttpClientFactory can be used in an app:
- Basic usage
- Named clients
- Typed clients
- Generated clients
The best approach depends upon the app's requirements.
Basic usage
Register IHttpClientFactory
by calling AddHttpClient
in Program.cs
:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// highlight-next-line
builder.Services.AddHttpClient();
An IHttpClientFactory
can be requested using dependency injection (DI). The following code uses IHttpClientFactory
to create an HttpClient
instance:
public class BasicModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
// highlight-next-line
public BasicModel(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ HeaderNames.Accept, "application/vnd.github.v3+json" },
{ HeaderNames.UserAgent, "HttpRequestsSample" }
}
};
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient();
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
Using IHttpClientFactory
like in the preceding example is a good way to refactor an existing app.
It has no impact on how HttpClient
is used. In places where HttpClient
instances are created in an existing app, replace those occurrences with calls to CreateClient
.
Named clients
Named clients are a good choice when:
- The app requires many distinct uses of
HttpClient
. - Many
HttpClients
have different configuration.
Specify configuration for a named HttpClient
during its registration in Program.cs
:
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
In the preceding code the client is configured with:
- The base address https://api.github.com/.
- Two headers required to work with the GitHub API.
CreateClient
Each time CreateClient is called:
- A new instance of
HttpClient
is created. - The configuration action is called.
To create a named client, pass its name into CreateClient
:
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
public NamedClientModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
In the preceding code, the request doesn't need to specify a hostname. The code can pass just the path, since the base address configured for the client is used.
Typed clients
Typed clients:
- Provide the same capabilities as named clients without the need to use strings as keys.
- Provides IntelliSense and compiler help when consuming clients.
- Provide a single location to configure and interact with a particular
HttpClient
. For example, a single typed client might be used:- For a single backend endpoint.
- To encapsulate all logic dealing with the endpoint.
- Work with DI and can be injected where required in the app.
A typed client accepts an HttpClient
parameter in its constructor:
public class GitHubService
{
private readonly HttpClient _httpClient;
// highlight-next-line
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
}
public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
"repos/dotnet/AspNetCore.Docs/branches");
}
In the preceding code:
- The configuration is moved into the typed client.
- The provided
HttpClient
instance is stored as a private field.
API-specific methods can be created that expose HttpClient
functionality. For example, the GetAspNetCoreDocsBranches
method encapsulates code to retrieve docs GitHub branches.
The following code calls AddHttpClient
in Program.cs
to register the GitHubService
typed client class:
builder.Services.AddHttpClient<GitHubService>();
The typed client is registered as transient with DI. In the preceding code, AddHttpClient
registers GitHubService
as a transient service. This registration uses a factory method to:
- Create an instance of
HttpClient
. - Create an instance of
GitHubService
, passing in the instance ofHttpClient
to its constructor.
The typed client can be injected and consumed directly:
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
// highlight-next-line
public TypedClientModel(GitHubService gitHubService) => _gitHubService = gitHubService;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
}
catch (HttpRequestException)
{
// ...
}
}
}
The configuration for a typed client can also be specified during its registration in Program.cs
, rather than in the typed client's constructor:
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// ...
});
Generated clients
IHttpClientFactory
can be used in combination with third-party libraries such as Refit. Refit is a REST library for .NET. It converts REST APIs into live interfaces.
Call AddRefitClient
to generate a dynamic implementation of an interface, which uses HttpClient
to make the external HTTP calls.
A custom interface represents the external API:
public interface IGitHubClient
{
[Get("/repos/dotnet/AspNetCore.Docs/branches")]
Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}
Call AddRefitClient
to generate the dynamic implementation and then call ConfigureHttpClient
to configure the underlying HttpClient
:
// highlight-next-line
builder.Services.AddRefitClient<IGitHubClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
Use DI to access the dynamic implementation of IGitHubClient
:
public class RefitModel : PageModel
{
private readonly IGitHubClient _gitHubClient;
// highlight-next-line
public RefitModel(IGitHubClient gitHubClient) => _gitHubClient = gitHubClient;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
}
catch (ApiException)
{
// ...
}
}
}
HttpClient request type
HttpClient supports other HTTP verbs:
Properties | Verbe |
---|---|
Delete | Represents an HTTP DELETE protocol method. |
Get | Represents an HTTP GET protocol method. |
Head | Represents an HTTP HEAD protocol method. The HEAD method is identical to GET except that the server only returns message-headers in the response, without a message-body. |
Method | An HTTP method. |
Options | Represents an HTTP OPTIONS protocol method. |
Patch | Gets the HTTP PATCH protocol method. |
Post | Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. |
Put | Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. |
Trace | Represents an HTTP TRACE protocol method. |