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.
Blazor/Documentation/docusaurus/docs/api/make-http-requests.md

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:

Sample Api Location

On Visual Studio, click right on the solution and choose Add => Existing Project...

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

Solution Properties

On the new screen select "Multiple startup projects" and select "Start" for the two projects.

Multiple startup 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:

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:

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 of HttpClient 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.