You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
14 KiB
448 lines
14 KiB
![]()
2 years ago
|
---
|
||
|
sidebar_position: 3
|
||
|
title: 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.
|
||
|
|
||
|
```csharp title="Models/Item.cs"
|
||
|
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:
|
||
|
|
||
|
```csharp title="Services/DataApiService.cs"
|
||
|
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:
|
||
|
|
||
|
```csharp title="Program.cs"
|
||
|
...
|
||
|
|
||
|
builder.Services.AddScoped<IDataService, DataApiService>();
|
||
|
|
||
|
...
|
||
|
```
|
||
|
|
||
|
## Add the API sample to your project
|
||
|
|
||
|
Download this [project](/Minecraft.Crafting.Api.zip).
|
||
|
|
||
|
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 in `HttpClient`.
|
||
|
* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing `HttpClient` 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`:
|
||
|
|
||
|
```csharp
|
||
|
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:
|
||
|
|
||
|
```csharp
|
||
|
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`:
|
||
|
|
||
|
```csharp
|
||
|
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`:
|
||
|
|
||
|
```csharp
|
||
|
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:
|
||
|
|
||
|
```csharp
|
||
|
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:
|
||
|
|
||
|
```csharp
|
||
|
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 of `HttpClient` to its constructor.
|
||
|
|
||
|
The typed client can be injected and consumed directly:
|
||
|
|
||
|
```csharp
|
||
|
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:
|
||
|
|
||
|
```csharp
|
||
|
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:
|
||
|
|
||
|
```csharp
|
||
|
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`:
|
||
|
|
||
|
```csharp
|
||
|
// 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`:
|
||
|
|
||
|
```csharp
|
||
|
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. |
|