Call a web API from ASP.NET Core Blazor

Note

This isn't the latest version of this article. For the current release, see the ASP.NET Core 8.0 version of this article.

This article describes how to call a web API from a Blazor app.

Throughout this article, the terms server/server-side and client/client-side are used to distinguish locations where app code executes:

  • Server/server-side: Interactive server-side rendering (interactive SSR) of a Blazor Web App.
  • Client/client-side
    • Client-side rendering (CSR) of a Blazor Web App.
    • A Blazor WebAssembly app.

Documentation component examples usually don't configure an interactive render mode with an @rendermode directive in the component's definition file (.razor):

  • In a Blazor Web App, the component must have an interactive render mode applied, either in the component's definition file or inherited from a parent component. For more information, see ASP.NET Core Blazor render modes.

  • In a standalone Blazor WebAssembly app, the components function as shown and don't require a render mode because components always run interactively on WebAssembly in a Blazor WebAssembly app.

When using the Interactive WebAssembly or Interactive Auto render modes, component code sent to the client can be decompiled and inspected. Don't place private code, app secrets, or other sensitive information in client-rendered components.

  • Server/server-side
    • The Server project of a hosted Blazor WebAssembly app.
    • A Blazor Server app.
  • Client/client-side
    • The Client project of a hosted Blazor WebAssembly app.
    • A Blazor WebAssembly app.

For guidance on the purpose and locations of files and folders, see ASP.NET Core Blazor project structure, which also describes the location of the Blazor start script and the location of <head> and <body> content.

The best way to run the demonstration code is to download the BlazorSample_{PROJECT TYPE} sample apps from the Blazor samples GitHub repository that matches the version of .NET that you're targeting. Not all of the documentation examples are currently in the sample apps, but an effort is currently underway to move most of the .NET 8 article examples into the .NET 8 sample apps. This work will be completed in the first quarter of 2024.

Note

The code examples in this article adopt nullable reference types (NRTs) and .NET compiler null-state static analysis, which are supported in ASP.NET Core in .NET 6 or later. When targeting ASP.NET Core 5.0 or earlier, remove the null type designation (?) from the string?, TodoItem[]?, WeatherForecast[]?, and IEnumerable<GitHubBranch>? types in the article's examples.

Note

This article has loaded WebAssembly client-side rendering (CSR) coverage for calling web APIs. The coverage for Server interactive server-side rendering (interactive SSR) addresses the following subjects:

  • Use of the HttpClient factory infrastructure to provide an HttpClient to the app.
  • Cross-Origin Resource Sharing (CORS) pertaining to server-side components.
  • Blazor framework component examples for testing web API access.
  • Additional resources for developing Blazor Server apps that call a web API.

Client-side components call web APIs using a preconfigured HttpClient service, which is focused on making requests back to the server of origin. Additional HttpClient service configurations for other web APIs can be created in developer code. Requests are composed using Blazor JSON helpers or with HttpRequestMessage. Requests can include Fetch API option configuration.

Examples in this article

In this article's component examples, a hypothetical todo list web API is used to create, read, update, and delete (CRUD) todo items on a server. The examples are based on a TodoItem class that stores the following todo item data:

  • ID (Id, long): Unique ID of the item.
  • Name (Name, string): Name of the item.
  • Status (IsComplete, bool): Indication if the todo item is finished.

Use the following TodoItem class with this article's examples if you build the examples into a test app:

public class TodoItem
{
    public long Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }
}

For guidance on how to create a server-side web API, see Tutorial: Create a web API with ASP.NET Core. For information on Cross-Origin Resource Sharing (CORS), see the Cross-Origin Resource Sharing (CORS) section later in this article.

The Blazor examples that demonstrate obtaining weather data from a server API are based on a hosted Blazor WebAssembly solution created from the Blazor WebAssembly project template.

For server-side components in Blazor Web Apps that require interactivity, add interactive server-side rendering (interactive SSR) to the component:

@rendermode InteractiveServer

For client-side components in Blazor Web Apps that require interactivity, add Interactive WebAssembly rendering to the component:

@rendermode InteractiveWebAssembly

Package

The System.Net.Http.Json package provides extension methods for System.Net.Http.HttpClient and System.Net.Http.HttpContent that perform automatic serialization and deserialization using System.Text.Json.

The System.Net.Http.Json package is provided by the .NET shared framework and doesn't require adding a package reference to the app.

Add the HttpClient service

This section covers implementation of the HttpClient service to make web API calls.

The configuration examples in this section are only useful when a single web API is called for a single HttpClient instance in the app. When the app must call multiple web APIs, each with its own base address and configuration, you can adopt the following approaches, which are covered later in this article:

In the Program file, add an HttpClient service if it isn't already present from a Blazor project template used to create the app:

builder.Services.AddScoped(sp => 
    new HttpClient
    {
        BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
    });

The preceding example sets the base address with builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), which gets the base address for the app and is typically derived from the <base> tag's href value in the host page.

The most common use cases for using the client's own base address are:

  • The client project (.Client) of a Blazor Web App (.NET 8 or later) makes web API calls from WebAssembly components or code that runs on the client in WebAssembly to APIs in the server app.
  • The client project (Client) of a hosted Blazor WebAssembly app makes web API calls to the server project (Server). Note that the hosted Blazor WebAssembly project template is no longer available in .NET 8 or later. However, hosted Blazor WebAssembly apps remain supported for .NET 8.

If you're calling an external web API (not in the same URL space as the client app), set the URI to the web API's base address. The following example sets the base address of the web API to https://localhost:5001, where a separate web API app is running and ready to respond to requests from the client app:

builder.Services.AddScoped(sp => 
    new HttpClient
    {
        BaseAddress = new Uri("https://localhost:5001")
    });

In most production apps, web API base addresses are managed via app settings, as the following examples demonstrate.

For local development, a separate web API app is running at a localhost address. The base address of the web API is assigned to a configuration key in the Development environment app settings file. The following examples use a key name of "ApiServer".

wwwroot/appsettings.Development.json:

{
  "ApiServer": "https://localhost:5001",
}

If a staging server is used, the staging web API base address is set in wwwroot/appsettings.Staging.json. The following example includes a version segment, which is a typical approach used to version web APIs:

{
  "ApiServer": "https://staging-api.contoso.com/v1.0",
}

The production URI is set in the Production environment app settings file, wwwroot/appsettings.Production.json:

{
  "ApiServer": "https://api.contoso.com/v1.0",
}

In the service registration, the appropriate app setting is read based on the app's environment:

builder.Services.AddScoped(sp => 
    new HttpClient
    {
        BaseAddress = new Uri(builder.Configuration["ApiServer"] ?? "http://0.0.0.0")
    });

Nested configuration settings are also commonly used in production apps because web API configuration usually requires additional configuration settings, such as configuring scopes for requests.

wwwroot/appsettings.Production.json:

"ApiServer": {
  "BaseUri": "https://api.contoso.com/v1.0",
  "Scopes": [
    "user.read"
  ]
}

Note

The configuration and web API calls described in this article only apply to public web APIs. For guidance on using HttpClient to make authorized web API requests in clients that authenticate users, see ASP.NET Core Blazor WebAssembly additional security scenarios after you've read this article. For a working example based on calling Microsoft Graph with a named HttpClient, see Use Graph API with ASP.NET Core Blazor WebAssembly.

In the service registration:

builder.Services.AddScoped(sp => 
    new HttpClient
    {
        BaseAddress = new Uri(
            builder.Configuration.GetSection("ApiServer")["BaseUri"] ?? 
                "http://0.0.0.0")
    });

Client-side services for HttpClient fail during prerendering

This section only applies to WebAssembly components in Blazor Web Apps.

Blazor Web Apps normally prerender client-side WebAssembly components. HttpClient services aren't registered by default in a Blazor Web App's main project. If the app is run with only the HttpClient services registered in the .Client project, as described in the Add the HttpClient service section, executing the app results in a runtime error:

InvalidOperationException: Cannot provide a value for property 'Http' on type '{ASSEMBLY}.Client.Pages.{COMPONENT}'. There is no registered service of type 'System.Net.Http.HttpClient'.

Use either of the following approaches to resolve this problem:

  • Add the HttpClient services to the main project to make them available during component prerendering. Use the following service registration in the main project's Program file:

    builder.Services.AddHttpClient();
    

    No explicit package reference is required for the main project because HttpClient services are provided by the shared framework.

  • If prerendering isn't required for the component, disable prerendering by following the guidance in ASP.NET Core Blazor render modes. If you adopt this approach, you don't need to add HttpClient services to the main project of the Blazor Web App.

For more information, see Client-side services fail to resolve during prerendering.

HttpClient and JSON helpers

HttpClient is available as a preconfigured service for making requests back to the origin server.

HttpClient and JSON helpers (System.Net.Http.Json.HttpClientJsonExtensions) are also used to call third-party web API endpoints. HttpClient is implemented using the browser's Fetch API and is subject to its limitations, including enforcement of the same-origin policy, which is discussed later in this article in the Cross-Origin Resource Sharing (CORS) section.

The client's base address is set to the originating server's address. Inject an HttpClient instance into a component using the @inject directive:

@using System.Net.Http
@inject HttpClient Http

Use the System.Net.Http.Json namespace for access to HttpClientJsonExtensions, including GetFromJsonAsync, PutAsJsonAsync, and PostAsJsonAsync:

@using System.Net.Http.Json

GET from JSON (GetFromJsonAsync)

GetFromJsonAsync sends an HTTP GET request and parses the JSON response body to create an object.

In the following component code, the todoItems are displayed by the component. GetFromJsonAsync is called when the component is finished initializing (OnInitializedAsync).

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

@inject HttpClient Http

@if (todoItems == null)
{
    <p>No Todo Items found.</p>
}
else
{
    <ul>
        @foreach (var item in todoItems)
        {
            <li>@item.Name</li>
        }
    </ul>
}

@code {
    private TodoItem[]? todoItems;

    protected override async Task OnInitializedAsync() => 
        todoItems = await Http.GetFromJsonAsync<TodoItem[]>("api/TodoItems");
}

POST as JSON (PostAsJsonAsync)

PostAsJsonAsync sends a POST request to the specified URI containing the value serialized as JSON in the request body.

In the following component code, newItemName is provided by a bound element of the component. The AddItem method is triggered by selecting a <button> element.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

@inject HttpClient Http

<input @bind="newItemName" placeholder="New Todo Item" />
<button @onclick="AddItem">Add</button>

@code {
    private string? newItemName;

    private async Task AddItem()
    {
        var addItem = new TodoItem { Name = newItemName, IsComplete = false };
        await Http.PostAsJsonAsync("api/TodoItems", addItem);
    }
}

PostAsJsonAsync returns an HttpResponseMessage. To deserialize the JSON content from the response message, use the ReadFromJsonAsync extension method. The following example reads JSON weather data as an array:

var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
    Array.Empty<WeatherForecast>();

In the preceding example, an empty array is created if no weather data is returned by the method, so content isn't null after the statement executes.

var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>();

PUT as JSON (PutAsJsonAsync)

PutAsJsonAsync sends an HTTP PUT request with JSON-encoded content.

In the following component code, editItem values for Name and IsCompleted are provided by bound elements of the component. The item's Id is set when the item is selected in another part of the UI (not shown) and EditItem is called. The SaveItem method is triggered by selecting the <button> element. The following example doesn't show loading todoItems for brevity. See the GET from JSON (GetFromJsonAsync) section for an example of loading items.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

@inject HttpClient Http

<input type="checkbox" @bind="editItem.IsComplete" />
<input @bind="editItem.Name" />
<button @onclick="SaveItem">Save</button>

@code {
    private string? id;
    private TodoItem editItem = new TodoItem();

    private void EditItem(long id)
    {
        editItem = todoItems.Single(i => i.Id == id);
    }

    private async Task SaveItem() =>
        await Http.PutAsJsonAsync($"api/TodoItems/{editItem.Id}", editItem);
}

PutAsJsonAsync returns an HttpResponseMessage. To deserialize the JSON content from the response message, use the ReadFromJsonAsync extension method. The following example reads JSON weather data as an array:

var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
    Array.Empty<WeatherForecast>();

In the preceding example, an empty array is created if no weather data is returned by the method, so content isn't null after the statement executes.

var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>();

PATCH as JSON (PatchAsJsonAsync)

PatchAsJsonAsync sends an HTTP PATCH request with JSON-encoded content.

In the following component code:

  • incompleteTodoItems is an array of incomplete TodoItem. The following example doesn't show loading incompleteTodoItems for brevity. See the GET from JSON (GetFromJsonAsync) section for an example of loading items.
  • The UpdateItem method is triggered by selecting the <button> element.
  • The PATCH document is provided as a plain text string. The web API described in the Tutorial: Create a web API with ASP.NET Core article doesn't handle PATCH requests by default. To make the PATCH example in this section work with the tutorial's web API, implement a PATCH controller action in the web API following the guidance in JsonPatch in ASP.NET Core web API. Later, this section demonstrates an example controller action and shows how to compose PATCH documents for ASP.NET Core web API apps that use .NET JSON PATCH support.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

@using System.Text.Json
@using System.Text.Json.Serialization
@inject HttpClient Http

<ul>
    @foreach (var item in incompleteTodoItems)
    {
        <li>
            @item.Name 
            <button @onclick="_ => UpdateItem(item.Id)">
                Mark 'Complete'
            </button>
        </li>
    }
</ul>

@code {
    private async Task UpdateItem(long id) =>
        await Http.PatchAsJsonAsync(
            $"api/TodoItems/{id}", 
            "[{\"operationType\":2,\"path\":\"/IsComplete\",\"op\":\"replace\",\"value\":true}]");
}

PatchAsJsonAsync returns an HttpResponseMessage. To deserialize the JSON content from the response message, use the ReadFromJsonAsync extension method. The following example reads JSON weather data as an array. An empty array is created if no weather data is returned by the method, so content isn't null after the statement executes:

var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
    Array.Empty<WeatherForecast>();

PatchAsJsonAsync receives a JSON PATCH document for the PATCH request. The preceding UpdateItem method called PatchAsJsonAsync with a PATCH document as a string with escaped quotes. Laid out with indentation, spacing, and non-escaped quotes, the unencoded PATCH document appears as the following JSON:

[
  {
    "operationType": 2,
    "path": "/IsComplete",
    "op": "replace",
    "value": true
  }
]

To simplify the creation of PATCH documents in the app issuing PATCH requests, an app can use .NET JSON PATCH support, as the following guidance demonstrates.

Install the Microsoft.AspNetCore.JsonPatch NuGet package and use the API features of the package to compose a JsonPatchDocument for a PATCH request.

Note

For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.

Add an @using directive for the Microsoft.AspNetCore.JsonPatch namespace to the top of the Razor component:

@using Microsoft.AspNetCore.JsonPatch

Compose the JsonPatchDocument for a TodoItem with IsComplete set to true using the Replace method:

var patchDocument = new JsonPatchDocument<TodoItem>()
    .Replace(p => p.IsComplete, true);

Pass the document's operations (patchDocument.Operations) to the PatchAsJsonAsync call. The following example shows how to make the call:

private async Task UpdateItem(long id)
{
    await Http.PatchAsJsonAsync(
        $"api/TodoItems/{id}", 
        patchDocument.Operations, 
        new JsonSerializerOptions()
        {
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
            WriteIndented = true
        });
}

JsonSerializerOptions.DefaultIgnoreCondition is set to JsonIgnoreCondition.WhenWritingDefault to ignore a property only if it equals the default value for its type.

JsonSerializerOptions.WriteIndented is used merely to present the JSON payload in a pleasant format for this article. Writing indented JSON has no bearing on processing PATCH requests and isn't typically performed in production apps for web API requests.

Next, follow the guidance in the JsonPatch in ASP.NET Core web API article to add a PATCH controller action to the web API.

Add a package reference for the Microsoft.AspNetCore.Mvc.NewtonsoftJson NuGet package to the web API app.

Note

There's no need to add a package reference for the Microsoft.AspNetCore.JsonPatch package to the app because the reference to the Microsoft.AspNetCore.Mvc.NewtonsoftJson package automatically transitively adds a package reference for Microsoft.AspNetCore.JsonPatch.

Add a custom JSON PATCH input formatter to the web API app.

JSONPatchInputFormatter.cs:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Options;

public static class JSONPatchInputFormatter
{
    public static NewtonsoftJsonPatchInputFormatter Get()
    {
        var builder = new ServiceCollection()
            .AddLogging()
            .AddMvc()
            .AddNewtonsoftJson()
            .Services.BuildServiceProvider();

        return builder
            .GetRequiredService<IOptions<MvcOptions>>()
            .Value
            .InputFormatters
            .OfType<NewtonsoftJsonPatchInputFormatter>()
            .First();
    }
}

Configure the web API's controllers to use the Microsoft.AspNetCore.Mvc.NewtonsoftJson package and process PATCH requests with the JSON PATCH input formatter. Insert the JSONPatchInputFormatter in the first position of MVC's input formatter collection so that it processes requests prior to any other input formatter.

In the Program file modify the call to AddControllers:

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, JSONPatchInputFormatter.Get());
}).AddNewtonsoftJson();

In Controllers/TodoItemsController.cs, add a using statement for the Microsoft.AspNetCore.JsonPatch namespace:

using Microsoft.AspNetCore.JsonPatch;

In Controllers/TodoItemsController.cs, add the following PatchTodoItem action method:

[HttpPatch("{id}")]
public async Task<IActionResult> PatchTodoItem(long id, 
    JsonPatchDocument<TodoItem> patchDoc)
{
    if (patchDoc == null)
    {
        return BadRequest();
    }

    var todoItem = await _context.TodoItems.FindAsync(id);

    if (todoItem == null)
    {
        return NotFound();
    }

    patchDoc.ApplyTo(todoItem);

    _context.Entry(todoItem).State = EntityState.Modified;

    try
    {
        await _context.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
    {
        return NotFound();
    }

    return NoContent();
}

Warning

As with the other examples in the JsonPatch in ASP.NET Core web API article, the preceding PATCH controller action doesn't protect the web API from over-posting attacks. For more information, see Tutorial: Create a web API with ASP.NET Core.

Additional extension methods

System.Net.Http includes additional extension methods for sending HTTP requests and receiving HTTP responses. HttpClient.DeleteAsync is used to send an HTTP DELETE request to a web API.

In the following component code, the <button> element calls the DeleteItem method. The bound <input> element supplies the id of the item to delete.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http and System.Threading.Tasks.

@inject HttpClient Http

<input @bind="id" />
<button @onclick="DeleteItem">Delete</button>

@code {
    private long id;

    private async Task DeleteItem() =>
        await Http.DeleteAsync($"api/TodoItems/{id}");
}

Named HttpClient with IHttpClientFactory

IHttpClientFactory services and the configuration of a named HttpClient are supported.

Note

An alternative to using a named HttpClient from an IHttpClientFactory is to use a typed HttpClient. For more information, see the Typed HttpClient section.

Add the Microsoft.Extensions.Http NuGet package to the app.

Note

For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.

In the Program file:

builder.Services.AddHttpClient("WebAPI", client => 
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

The preceding example sets the base address with builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), which gets the base address for the app and is typically derived from the <base> tag's href value in the host page.

The most common use cases for using the client's own base address are:

  • The client project (.Client) of a Blazor Web App (.NET 8 or later) makes web API calls from WebAssembly components or code that runs on the client in WebAssembly to APIs in the server app.
  • The client project (Client) of a hosted Blazor WebAssembly app makes web API calls to the server project (Server).

If you're calling an external web API (not in the same URL space as the client app), set the URI to the web API's base address. The following example sets the base address of the web API to https://localhost:5001, where a separate web API app is running and ready to respond to requests from the client app:

builder.Services.AddHttpClient("WebAPI", client => 
    client.BaseAddress = new Uri(https://localhost:5001));

In the following component code:

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

FetchDataViaFactory.razor:

@page "/fetch-data-via-factory"
@using {PROJECT NAME}.Shared
@inject IHttpClientFactory ClientFactory

<h1>Fetch data via <code>IHttpClientFactory</code></h1>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h2>Temperatures by Date</h2>

    <ul>
        @foreach (var forecast in forecasts)
        {
            <li>
                @forecast.Date.ToShortDateString():
                @forecast.TemperatureC &#8451;
                @forecast.TemperatureF &#8457;
            </li>
        }
    </ul>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        var client = ClientFactory.CreateClient("WebAPI");

        forecasts = await client.GetFromJsonAsync<WeatherForecast[]>(
            "WeatherForecast");
    }
}

For an additional example based on calling Microsoft Graph with a named HttpClient, see Use Graph API with ASP.NET Core Blazor WebAssembly.

Typed HttpClient

Typed HttpClient uses one or more of the app's HttpClient instances, default or named, to return data from one or more web API endpoints.

Note

An alternative to using a typed HttpClient is to use a named HttpClient from an IHttpClientFactory. For more information, see the Named HttpClient with IHttpClientFactory section.

Add the Microsoft.Extensions.Http NuGet package to the app.

Note

For guidance on adding packages to .NET apps, see the articles under Install and manage packages at Package consumption workflow (NuGet documentation). Confirm correct package versions at NuGet.org.

WeatherForecastHttpClient.cs:

using System.Net.Http.Json;

namespace BlazorSample.Client;

public class WeatherForecastHttpClient(HttpClient http)
{
    private readonly HttpClient http = http;
    private WeatherForecast[]? forecasts;

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
            "WeatherForecast");

        return forecasts ?? [];
    }
}
using System.Net.Http.Json;

public class WeatherForecastHttpClient
{
    private readonly HttpClient http;
    private WeatherForecast[]? forecasts;

    public WeatherForecastHttpClient(HttpClient http)
    {
        this.http = http;
    }

    public async Task<WeatherForecast[]> GetForecastAsync()
    {
        forecasts = await http.GetFromJsonAsync<WeatherForecast[]>(
            "WeatherForecast");

        return forecasts ?? Array.Empty<WeatherForecast>();
    }
}

In the Program file:

builder.Services.AddHttpClient<WeatherForecastHttpClient>(client => 
    client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));

The preceding example sets the base address with builder.HostEnvironment.BaseAddress (IWebAssemblyHostEnvironment.BaseAddress), which gets the base address for the app and is typically derived from the <base> tag's href value in the host page.

The most common use cases for using the client's own base address are:

  • The client project (.Client) of a Blazor Web App (.NET 8 or later) makes web API calls from WebAssembly components or code that runs on the client in WebAssembly to APIs in the server app.
  • The client project (Client) of a hosted Blazor WebAssembly app makes web API calls to the server project (Server).

If you're calling an external web API (not in the same URL space as the client app), set the URI to the web API's base address. The following example sets the base address of the web API to https://localhost:5001, where a separate web API app is running and ready to respond to requests from the client app:

builder.Services.AddHttpClient<WeatherForecastHttpClient>(client => 
    client.BaseAddress = new Uri(https://localhost:5001));

Components inject the typed HttpClient to call the web API.

In the following component code:

  • An instance of the preceding WeatherForecastHttpClient is injected, which creates a typed HttpClient.
  • The typed HttpClient is used to issue a GET request for JSON weather forecast data from the web API.

Note

When targeting ASP.NET Core 5.0 or earlier, add an @using directive to the following component for System.Threading.Tasks.

FetchDataViaTypedHttpClient.razor:

@page "/fetch-data-via-typed-httpclient"
@using {PROJECT NAME}.Shared
@inject WeatherForecastHttpClient Http

<h1>Fetch data via typed <code>HttpClient</code></h1>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h2>Temperatures by Date</h2>

    <ul>
        @foreach (var forecast in forecasts)
        {
            <li>
                @forecast.Date.ToShortDateString():
                @forecast.TemperatureC &#8451;
                @forecast.TemperatureF &#8457;
            </li>
        }
    </ul>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetForecastAsync();
    }
}

HttpClient and HttpRequestMessage with Fetch API request options

HttpClient (API documentation) and HttpRequestMessage can be used to customize requests. For example, you can specify the HTTP method and request headers. The following component makes a POST request to a web API endpoint and shows the response body.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http and System.Net.Http.Json.

TodoRequest.razor:

@page "/todo-request"
@using System.Net.Http.Headers
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@inject HttpClient Http
@inject IAccessTokenProvider TokenProvider

<h1>ToDo Request</h1>

<h1>ToDo Request Example</h1>

<button @onclick="PostRequest">Submit POST request</button>

<p>Response body returned by the server:</p>

<p>@responseBody</p>

@code {
    private string? responseBody;

    private async Task PostRequest()
    {
        var requestMessage = new HttpRequestMessage()
        {
            Method = new HttpMethod("POST"),
            RequestUri = new Uri("https://localhost:10000/api/TodoItems"),
            Content =
                JsonContent.Create(new TodoItem
                {
                    Name = "My New Todo Item",
                    IsComplete = false
                })
        };

        var tokenResult = await TokenProvider.RequestAccessToken();

        if (tokenResult.TryGetToken(out var token))
        {
            requestMessage.Headers.Authorization =
                new AuthenticationHeaderValue("Bearer", token.Value);

            requestMessage.Content.Headers.TryAddWithoutValidation(
                "x-custom-header", "value");

            var response = await Http.SendAsync(requestMessage);
            var responseStatusCode = response.StatusCode;

            responseBody = await response.Content.ReadAsStringAsync();
        }
    }

    public class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

Blazor's client-side implementation of HttpClient uses Fetch API and configures the underlying request-specific Fetch API options via HttpRequestMessage extension methods and WebAssemblyHttpRequestMessageExtensions. Set additional options using the generic SetBrowserRequestOption extension method. Blazor and the underlying Fetch API don't directly add or modify request headers. For more information on how user agents, such as browsers, interact with headers, consult external user agent documentation sets and other web resources.

The HTTP response is typically buffered to enable support for synchronous reads on the response content. To enable support for response streaming, use the SetBrowserResponseStreamingEnabled extension method on the request.

To include credentials in a cross-origin request, use the SetBrowserRequestCredentials extension method:

requestMessage.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

For more information on Fetch API options, see MDN web docs: WindowOrWorkerGlobalScope.fetch(): Parameters.

Call web API example

The following example calls a web API. The example requires a running web API based on the sample app described by the Tutorial: Create a web API with ASP.NET Core article. This example makes requests to the web API at https://localhost:10000/api/TodoItems. If a different web API address is used, update the ServiceEndpoint constant value in the component's @code block.

The following example makes a Cross-Origin Resource Sharing (CORS) request from http://localhost:5000 or https://localhost:5001 to the web API. Add the following CORS Middleware configuration to the web API's service's Program file:

app.UseCors(policy => 
    policy.WithOrigins("http://localhost:5000", "https://localhost:5001")
    .AllowAnyMethod()
    .WithHeaders(HeaderNames.ContentType));

Adjust the domains and ports of WithOrigins as needed for the Blazor app. For more information, see Enable Cross-Origin Requests (CORS) in ASP.NET Core.

By default, ASP.NET Core apps use ports 5000 (HTTP) and 5001 (HTTPS). To run both apps on the same machine at the same time for testing, use a different port for the web API app (for example, port 10000). For more information on setting the port, see Configure endpoints for the ASP.NET Core Kestrel web server.

CallWebAPI.razor:

@page "/call-web-api"
@inject HttpClient Http

<PageTitle>Call web API</PageTitle>

<h1>Call web API Example</h1>

@if (todoItems == null)
{
    <p>No Todo Items found.</p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th class="text-center">Complete</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr id="editRow" style="display:@editRowStyle">
                <td class="text-center">
                    <input type="checkbox" @bind="editItem.IsComplete" />
                </td>
                <td>
                    <input @bind="editItem.Name" />
                </td>
                <td class="text-center">
                    <button class="btn btn-success" @onclick="SaveItem">
                        Save
                    </button>
                    <button class="btn btn-danger" 
                            @onclick="@(() => editRowStyle = "none")">
                        Cancel
                    </button>
                </td>
            </tr>
            @foreach (var item in todoItems)
            {
                <tr>
                    <td class="text-center">
                        @if (item.IsComplete)
                        {
                            <span>✔</span>
                        }
                    </td>
                    <td>@item.Name</td>
                    <td class="text-center">
                        <button class="btn btn-warning" 
                                @onclick="@(() => EditItem(item.Id))">
                            Edit
                        </button>
                        <button class="btn btn-danger" 
                                @onclick="@(async () => await DeleteItem(item.Id))">
                            Delete
                        </button>
                    </td>
                </tr>
            }
            <tr id="addRow">
                <td></td>
                <td>
                    <input @bind="newItemName" placeholder="New Todo Item" />
                </td>
                <td class="text-center">
                    <button class="btn btn-success" @onclick="AddItem">Add</button>
                </td>
            </tr>
        </tbody>
    </table>
}

@code {
    private const string ServiceEndpoint = "https://localhost:10000/api/TodoItems";
    private TodoItem[]? todoItems;
    private TodoItem editItem = new();
    private string editRowStyle = "none";
    private string? newItemName;

    protected override async Task OnInitializedAsync() => await GetTodoItems();

    private async Task GetTodoItems() => 
        todoItems = await Http.GetFromJsonAsync<TodoItem[]>(ServiceEndpoint);

    private void EditItem(long id)
    {
        if (todoItems is not null)
        {
            editItem = todoItems.Single(i => i.Id == id);
            editRowStyle = "table-row";
        }
    }

    private async Task AddItem()
    {
        var addItem = new TodoItem { Name = newItemName, IsComplete = false };
        await Http.PostAsJsonAsync(ServiceEndpoint, addItem);
        newItemName = string.Empty;
        await GetTodoItems();
        editRowStyle = "none";
    }

    private async Task SaveItem()
    {
        if (editItem is not null)
        {
            await Http.PutAsJsonAsync($"{ServiceEndpoint}/{editItem.Id}", 
                editItem);
        }
        await GetTodoItems();
        editRowStyle = "none";
    }

    private async Task DeleteItem(long id)
    {
        await Http.DeleteAsync($"{ServiceEndpoint}/{id}");
        await GetTodoItems();
        editRowStyle = "none";
    }

    private class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    }
}
@page "/call-web-api"
@inject HttpClient Http

<h1>Todo Items</h1>

@if (todoItems == null)
{
    <p>No Todo Items found.</p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th class="text-center">Complete</th>
                <th>Name</th>
                <th></th>
            </tr>
        </thead>
        <tbody>
            <tr id="editRow" style="display:@editRowStyle">
                <td class="text-center">
                    <input type="checkbox" @bind="editItem.IsComplete" />
                </td>
                <td>
                    <input @bind="editItem.Name" />
                </td>
                <td class="text-center">
                    <button class="btn btn-success" @onclick="SaveItem">
                        Save
                    </button>
                    <button class="btn btn-danger" 
                            @onclick="@(() => editRowStyle = "none")">
                        Cancel
                    </button>
                </td>
            </tr>
            @foreach (var item in todoItems)
            {
                <tr>
                    <td class="text-center">
                        @if (item.IsComplete)
                        {
                            <span>✔</span>
                        }
                    </td>
                    <td>@item.Name</td>
                    <td class="text-center">
                        <button class="btn btn-warning" 
                                @onclick="@(() => EditItem(item.Id))">
                            Edit
                        </button>
                        <button class="btn btn-danger" 
                                @onclick="@(async () => await DeleteItem(item.Id))">
                            Delete
                        </button>
                    </td>
                </tr>
            }
            <tr id="addRow">
                <td></td>
                <td>
                    <input @bind="newItemName" placeholder="New Todo Item" />
                </td>
                <td class="text-center">
                    <button class="btn btn-success" @onclick="AddItem">Add</button>
                </td>
            </tr>
        </tbody>
    </table>
}

@code {
    private const string ServiceEndpoint = "https://localhost:10000/api/TodoItems";
    private TodoItem[]? todoItems;
    private TodoItem editItem = new();
    private string editRowStyle = "none";
    private string? newItemName;

    protected override async Task OnInitializedAsync() => await GetTodoItems();

    private async Task GetTodoItems() => 
        todoItems = await Http.GetFromJsonAsync<TodoItem[]>(ServiceEndpoint);

    private void EditItem(long id)
    {
        if (todoItems is not null)
        {
            editItem = todoItems.Single(i => i.Id == id);
            editRowStyle = "table-row";
        }
    }

    private async Task AddItem()
    {
        var addItem = new TodoItem { Name = newItemName, IsComplete = false };
        await Http.PostAsJsonAsync(ServiceEndpoint, addItem);
        newItemName = string.Empty;
        await GetTodoItems();
        editRowStyle = "none";
    }

    private async Task SaveItem()
    {
        if (editItem is not null)
        {
            await Http.PutAsJsonAsync($"{ServiceEndpoint}/{editItem.Id}", 
                editItem);
        }
        await GetTodoItems();
        editRowStyle = "none";
    }

    private async Task DeleteItem(long id)
    {
        await Http.DeleteAsync($"{ServiceEndpoint}/{id}");
        await GetTodoItems();
        editRowStyle = "none";
    }

    private class TodoItem
    {
        public long Id { get; set; }
        public string? Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

Handle errors

Handle web API response errors in developer code when they occur. For example, GetFromJsonAsync expects a JSON response from the web API with a Content-Type of application/json. If the response isn't in JSON format, content validation throws a NotSupportedException.

In the following example, the URI endpoint for the weather forecast data request is misspelled. The URI should be to WeatherForecast but appears in the call as WeatherForcast, which is missing the letter e in Forecast.

The GetFromJsonAsync call expects JSON to be returned, but the web API returns HTML for an unhandled exception with a Content-Type of text/html. The unhandled exception occurs because the path to /WeatherForcast isn't found and middleware can't serve a page or view for the request.

In OnInitializedAsync on the client, NotSupportedException is thrown when the response content is validated as non-JSON. The exception is caught in the catch block, where custom logic could log the error or present a friendly error message to the user.

Note

When targeting ASP.NET Core 5.0 or earlier, add @using directives to the following component for System.Net.Http, System.Net.Http.Json, and System.Threading.Tasks.

ReturnHTMLOnException.razor:

@page "/return-html-on-exception"
@using {PROJECT NAME}.Shared
@inject HttpClient Http

<h1>Fetch data but receive HTML on unhandled exception</h1>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <h2>Temperatures by Date</h2>

    <ul>
        @foreach (var forecast in forecasts)
        {
            <li>
                @forecast.Date.ToShortDateString():
                @forecast.TemperatureC &#8451;
                @forecast.TemperatureF &#8457;
            </li>
        }
    </ul>
}

<p>
    @exceptionMessage
</p>

@code {
    private WeatherForecast[]? forecasts;
    private string? exceptionMessage;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            // The URI endpoint "WeatherForecast" is misspelled on purpose on the 
            // next line. See the preceding text for more information.
            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForcast");
        }
        catch (NotSupportedException exception)
        {
            exceptionMessage = exception.Message;
        }
    }
}

Note

The preceding example is for demonstration purposes. A web API can be configured to return JSON even when an endpoint doesn't exist or an unhandled exception occurs on the server.

For more information, see Handle errors in ASP.NET Core Blazor apps.

Note

This article has loaded Server interactive server-side rendering (interactive SSR) coverage for calling web APIs. The WebAssembly client-side rendering (CSR) coverage addresses the following subjects:

  • Client-side examples that call a web API to create, read, update, and delete todo list items.
  • System.Net.Http.Json package.
  • HttpClient service configuration.
  • HttpClient and JSON helpers (GetFromJsonAsync, PostAsJsonAsync, PutAsJsonAsync, DeleteAsync).
  • IHttpClientFactory services and the configuration of a named HttpClient.
  • Typed HttpClient.
  • HttpClient and HttpRequestMessage to customize requests.
  • Call web API example with Cross-Origin Resource Sharing (CORS) and how CORS pertains to client-side components.
  • How to handle web API response errors in developer code.
  • Blazor framework component examples for testing web API access.
  • Additional resources for developing client-side components that call a web API.

Server-based components call web APIs using HttpClient instances, typically created using IHttpClientFactory. For guidance that applies to server-side apps, see Make HTTP requests using IHttpClientFactory in ASP.NET Core.

A server-side app doesn't include an HttpClient service by default. Provide an HttpClient to the app using the HttpClient factory infrastructure.

In the Program file:

builder.Services.AddHttpClient();

The following Razor component makes a request to a web API for GitHub branches similar to the Basic Usage example in the Make HTTP requests using IHttpClientFactory in ASP.NET Core article.

CallWebAPI.razor:

@page "/call-web-api"
@using System.Text.Json
@using System.Text.Json.Serialization
@inject IHttpClientFactory ClientFactory

<h1>Call web API from a Blazor Server Razor component</h1>

@if (getBranchesError || branches is null)
{
    <p>Unable to get branches from GitHub. Please try again later.</p>
}
else
{
    <ul>
        @foreach (var branch in branches)
        {
            <li>@branch.Name</li>
        }
    </ul>
}

@code {
    private IEnumerable<GitHubBranch>? branches = Array.Empty<GitHubBranch>();
    private bool getBranchesError;
    private bool shouldRender;

    protected override bool ShouldRender() => shouldRender;

    protected override async Task OnInitializedAsync()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = ClientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            getBranchesError = true;
        }

        shouldRender = true;
    }

    public class GitHubBranch
    {
        [JsonPropertyName("name")]
        public string? Name { get; set; }
    }
}

For an additional working example, see the server-side file upload example that uploads files to a web API controller in the ASP.NET Core Blazor file uploads article.

Cross-Origin Resource Sharing (CORS)

Browser security restricts a webpage from making requests to a different domain than the one that served the webpage. This restriction is called the same-origin policy. The same-origin policy restricts (but doesn't prevent) a malicious site from reading sensitive data from another site. To make requests from the browser to an endpoint with a different origin, the endpoint must enable Cross-Origin Resource Sharing (CORS).

For information on client-side CORS requests, see ASP.NET Core Blazor WebAssembly additional security scenarios.

For information on CORS, see Enable Cross-Origin Requests (CORS) in ASP.NET Core. The article's examples don't pertain directly to Razor component scenarios, but the article is useful for learning general CORS concepts.

Antiforgery support

To add antiforgery support to an HTTP request, inject the AntiforgeryStateProvider and add a RequestToken to the headers collection as a RequestVerificationToken:

@inject AntiforgeryStateProvider Antiforgery
private async Task OnSubmit()
{
    var antiforgery = Antiforgery.GetAntiforgeryToken();
    var request = new HttpRequestMessage(HttpMethod.Post, "action");
    request.Headers.Add("RequestVerificationToken", antiforgery.RequestToken);
    var response = await client.SendAsync(request);
    ...
}

For more information, see ASP.NET Core Blazor authentication and authorization.

Blazor framework component examples for testing web API access

Various network tools are publicly available for testing web API backend apps directly, such as Firefox Browser Developer and Postman. Blazor framework's reference source includes HttpClient test assets that are useful for testing:

HttpClientTest assets in the dotnet/aspnetcore GitHub repository

Note

Documentation links to .NET reference source usually load the repository's default branch, which represents the current development for the next release of .NET. To select a tag for a specific release, use the Switch branches or tags dropdown list. For more information, see How to select a version tag of ASP.NET Core source code (dotnet/AspNetCore.Docs #26205).

Additional resources