PATCH section and other updates (#32007)

pull/32006/head
Luke Latham 2024-03-08 08:33:30 -05:00 committed by GitHub
parent c15c3aa8fa
commit d9ab1c3233
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 51 additions and 162 deletions

View File

@ -5,7 +5,7 @@ description: Learn how to call a web API from Blazor apps.
monikerRange: '>= aspnetcore-3.1'
ms.author: riande
ms.custom: mvc
ms.date: 03/07/2024
ms.date: 03/08/2024
uid: blazor/call-web-api
---
# Call a web API from ASP.NET Core Blazor
@ -136,7 +136,7 @@ else
}
@code {
private IEnumerable<GitHubBranch>? branches = Array.Empty<GitHubBranch>();
private IEnumerable<GitHubBranch>? branches = [];
private bool getBranchesError;
private bool shouldRender;
@ -175,6 +175,8 @@ else
}
```
In the preceding example for C# 12 or later, an empty array (`[]`) is created for the `branches` variable. For earlier versions of C#, create an empty array (`Array.Empty<GitHubBranch>()`).
For an additional working example, see the server-side file upload example that uploads files to a web API controller in the <xref:blazor/file-uploads#upload-files-to-a-server-with-server-side-rendering> article.
:::moniker range=">= aspnetcore-8.0"
@ -297,28 +299,31 @@ Use the <xref:System.Net.Http.Json?displayProperty=fullName> namespace for acces
@using System.Net.Http.Json
```
### GET from JSON (`GetFromJsonAsync`)
The following sections cover JSON helpers:
* [GET](#get-from-json-getfromjsonasync)
* [POST](#post-as-json-postasjsonasync)
* [PUT](#put-as-json-putasjsonasync)
* [PATCH](#patch-as-json-patchasjsonasync)
<xref:System.Net.Http> includes additional methods for sending HTTP requests and receiving HTTP responses, for example to send a DELETE request. For more information, see the [DELETE and additional extension methods](#delete-deleteasync-and-additional-extension-methods) section.
## GET from JSON (`GetFromJsonAsync`)
<xref:System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsync%2A> 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. <xref:System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsync%2A> is called when the component is finished initializing ([`OnInitializedAsync`](xref:blazor/components/lifecycle#component-initialization-oninitializedasync)).
> [!NOTE]
> When targeting ASP.NET Core 5.0 or earlier, add `@using` directives to the following component for <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
```csharp
todoItems = await Http.GetFromJsonAsync<TodoItem[]>("todoitems");
```
### POST as JSON (`PostAsJsonAsync`)
## POST as JSON (`PostAsJsonAsync`)
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PostAsJsonAsync%2A> 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 <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
```csharp
await Http.PostAsJsonAsync("todoitems", addItem);
```
@ -326,20 +331,16 @@ await Http.PostAsJsonAsync("todoitems", addItem);
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PostAsJsonAsync%2A> returns an <xref:System.Net.Http.HttpResponseMessage>. To deserialize the JSON content from the response message, use the <xref:System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync%2A> extension method. The following example reads JSON weather data as an array:
```csharp
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? [];
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
Array.Empty<WeatherForecast>();
```
In the preceding example for C# 12 or later, an empty array (`[]`) is created if no weather data is returned by the method, so `content` isn't null after the statement executes but returns. In earlier versions of C#, create an empty array (`Array.Empty<WeatherForecast>()`).
### PUT as JSON (`PutAsJsonAsync`)
## PUT as JSON (`PutAsJsonAsync`)
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PutAsJsonAsync%2A> 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`)](#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 <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
```csharp
await Http.PutAsJsonAsync($"todoitems/{editItem.Id}", editItem);
```
@ -347,79 +348,36 @@ await Http.PutAsJsonAsync($"todoitems/{editItem.Id}", editItem);
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PutAsJsonAsync%2A> returns an <xref:System.Net.Http.HttpResponseMessage>. To deserialize the JSON content from the response message, use the <xref:System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync%2A> extension method. The following example reads JSON weather data as an array:
```csharp
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ?? [];
var content = await response.Content.ReadFromJsonAsync<WeatherForecast[]>() ??
Array.Empty<WeatherForecast>();
```
In the preceding example for C# 12 or later, an empty array (`[]`) is created if no weather data is returned by the method, so `content` isn't null after the statement executes but returns. In earlier versions of C#, create an empty array (`Array.Empty<WeatherForecast>()`).
:::moniker range=">= aspnetcore-7.0"
### PATCH as JSON (`PatchAsJsonAsync`)
> [!NOTE]
> This section's example isn't currently demonstrated in the sample apps (`BlazorWebAppCallWebApi`/`BlazorWebAssemblyCallWebApi`, .NET 8 or later).
## PATCH as JSON (`PatchAsJsonAsync`)
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> sends an HTTP PATCH request with JSON-encoded content.
This section's examples are based on a `TodoItem` class that stores the following todo item data:
> [!NOTE]
> For more information, see <xref:web-api/jsonpatch>.
* 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.
`TodoItem` class:
In the following example, <xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> receives a JSON PATCH document as a plain text string with escaped quotes:
```csharp
public class TodoItem
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}
```
In the following component code:
* `incompleteTodoItems` is an array of incomplete `TodoItem`. The following example doesn't show loading `incompleteTodoItems` for brevity. Loading items is covered in the [GET from JSON (`GetFromJsonAsync`)](#get-from-json-getfromjsonasync) section.
* 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 <xref:tutorials/first-web-api> 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 <xref:web-api/jsonpatch>. 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 <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
```razor
@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(
$"todoitems/{id}",
"[{\"operationType\":2,\"path\":\"/IsComplete\",\"op\":\"replace\",\"value\":true}]");
}
await Http.PatchAsJsonAsync(
$"todoitems/{id}",
"[{\"operationType\":2,\"path\":\"/IsComplete\",\"op\":\"replace\",\"value\":true}]");
```
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> returns an <xref:System.Net.Http.HttpResponseMessage>. To deserialize the JSON content from the response message, use the <xref:System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync%2A> extension method. The following example reads JSON todo item data as an array. An empty array is created if no item data is returned by the method, so `content` isn't null after the statement executes:
```csharp
var response = await Http.PatchAsJsonAsync(...);
var content = await response.Content.ReadFromJsonAsync<TodoItem[]>() ??
Array.Empty<TodoItem>();
```
<xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> receives a JSON PATCH document for the PATCH request. The preceding `UpdateItem` method called <xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> 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:
Laid out with indentation, spacing, and unescaped quotes, the unencoded PATCH document appears as the following JSON:
```json
[
@ -438,9 +396,11 @@ Install the [`Microsoft.AspNetCore.JsonPatch`](https://www.nuget.org/packages/Mi
[!INCLUDE[](~/includes/package-reference.md)]
Add an `@using` directive for the <xref:Microsoft.AspNetCore.JsonPatch?displayProperty=fullName> namespace to the top of the Razor component:
Add `@using` directives for the <xref:System.Text.Json?displayProperty=fullName>, <xref:System.Text.Json.Serialization?displayProperty=fullName>, and <xref:Microsoft.AspNetCore.JsonPatch?displayProperty=fullName> namespaces to the top of the Razor component:
```razor
@using System.Text.Json
@using System.Text.Json.Serialization
@using Microsoft.AspNetCore.JsonPatch
```
@ -451,7 +411,7 @@ var patchDocument = new JsonPatchDocument<TodoItem>()
.Replace(p => p.IsComplete, true);
```
Pass the document's operations (`patchDocument.Operations`) to the <xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> call. The following example shows how to make the call:
Pass the document's operations (`patchDocument.Operations`) to the <xref:System.Net.Http.Json.HttpClientJsonExtensions.PatchAsJsonAsync%2A> call:
```csharp
private async Task UpdateItem(long id)
@ -461,119 +421,60 @@ private async Task UpdateItem(long id)
patchDocument.Operations,
new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
WriteIndented = true
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault
});
}
```
<xref:System.Text.Json.JsonSerializerOptions.DefaultIgnoreCondition?displayProperty=nameWithType> is set to <xref:System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault?displayProperty=nameWithType> to ignore a property only if it equals the default value for its type.
<xref:System.Text.Json.JsonSerializerOptions.WriteIndented?displayProperty=nameWithType> 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.
Add <xref:System.Text.Json.JsonSerializerOptions.WriteIndented?displayProperty=nameWithType> set to `true` if you want to present the JSON payload in a pleasant format for display. 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 <xref:web-api/jsonpatch> article to add a PATCH controller action to the web API, which is reproduced here in the following steps.
Follow the guidance in the <xref:web-api/jsonpatch> article to add a PATCH controller action to the web API. Alternatively, PATCH request processing can be implemented as a [Minimal API](xref:fundamentals/minimal-apis) with the following steps.
Add a package reference for the [`Microsoft.AspNetCore.Mvc.NewtonsoftJson`](https://www.nuget.org/packages/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`](https://www.nuget.org/packages/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`:
```csharp
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 <xref:Microsoft.Extensions.DependencyInjection.MvcServiceCollectionExtensions.AddControllers%2A>:
```csharp
builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0, JSONPatchInputFormatter.Get());
}).AddNewtonsoftJson();
```
In `Controllers/TodoItemsController.cs`, add a `using` statement for the <xref:Microsoft.AspNetCore.JsonPatch?displayProperty=fullName> namespace:
In the `Program` file add an `@using` directive for the <xref:Microsoft.AspNetCore.JsonPatch?displayProperty=fullName> namespace:
```csharp
using Microsoft.AspNetCore.JsonPatch;
```
In `Controllers/TodoItemsController.cs`, add the following `PatchTodoItem` action method:
Provide the endpoint to the request processing pipeline of the web API:
```csharp
[HttpPatch("{id}")]
public async Task<IActionResult> PatchTodoItem(long id,
JsonPatchDocument<TodoItem> patchDoc)
app.MapPatch("/todoitems/{id}", async (long id, TodoContext db) =>
{
if (patchDoc == null)
if (await db.TodoItems.FindAsync(id) is TodoItem todo)
{
return BadRequest();
}
var todoItem = await _context.TodoItems.FindAsync(id);
var patchDocument =
new JsonPatchDocument<TodoItem>().Replace(p => p.IsComplete, true);
patchDocument.ApplyTo(todo);
await db.SaveChangesAsync();
if (todoItem == null)
{
return NotFound();
return TypedResults.Ok(todo);
}
patchDoc.ApplyTo(todoItem);
_context.Entry(todoItem).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
{
return NotFound();
}
return NoContent();
}
return TypedResults.NoContent();
});
```
> [!WARNING]
> As with the other examples in the <xref:web-api/jsonpatch> article, the preceding PATCH controller action doesn't protect the web API from over-posting attacks. For more information, see <xref:tutorials/first-web-api#prevent-over-posting>.
> As with the other examples in the <xref:web-api/jsonpatch> article, the preceding PATCH API doesn't protect the web API from over-posting attacks. For more information, see <xref:tutorials/first-web-api#prevent-over-posting>.
For a fully working PATCH experience, see the `BlazorWebAppCallWebApi` [sample app](#sample-apps).
:::moniker-end
### Additional extension methods
## DELETE (`DeleteAsync`) and additional extension methods
<xref:System.Net.Http> includes additional extension methods for sending HTTP requests and receiving HTTP responses. <xref:System.Net.Http.HttpClient.DeleteAsync%2A?displayProperty=nameWithType> 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 <xref:System.Net.Http?displayProperty=fullName> and <xref:System.Threading.Tasks?displayProperty=fullName>.
```csharp
await Http.DeleteAsync($"todoitems/{id}");
```
@ -631,9 +532,6 @@ In the following component code:
* An instance of <xref:System.Net.Http.IHttpClientFactory> creates a named <xref:System.Net.Http.HttpClient>.
* The named <xref:System.Net.Http.HttpClient> is used to issue a GET request for JSON weather forecast data from the web API at `/forecast`.
> [!NOTE]
> When targeting ASP.NET Core 5.0 or earlier, add `@using` directives to the following component for <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
```razor
@inject IHttpClientFactory ClientFactory
@ -736,9 +634,6 @@ In the following component code:
* An instance of the preceding `ForecastHttpClient` is injected, which creates a typed <xref:System.Net.Http.HttpClient>.
* The typed <xref:System.Net.Http.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 <xref:System.Threading.Tasks?displayProperty=fullName>.
```razor
@inject ForecastHttpClient Http
@ -766,9 +661,6 @@ The `BlazorWebAppCallWebApi` [sample app](#sample-apps) demonstrates calling a w
[`HttpClient`](xref:fundamentals/http-requests) ([API documentation](xref:System.Net.Http.HttpClient)) and <xref:System.Net.Http.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 <xref:System.Net.Http?displayProperty=fullName> and <xref:System.Net.Http.Json?displayProperty=fullName>.
`TodoRequest.razor`:
```razor
@ -853,9 +745,6 @@ The <xref:System.Net.Http.Json.HttpClientJsonExtensions.GetFromJsonAsync%2A> cal
In <xref:Microsoft.AspNetCore.Components.ComponentBase.OnInitializedAsync%2A> on the client, <xref:System.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 <xref:System.Net.Http?displayProperty=fullName>, <xref:System.Net.Http.Json?displayProperty=fullName>, and <xref:System.Threading.Tasks?displayProperty=fullName>.
`ReturnHTMLOnException.razor`:
```razor