Skip to content

Commit 3e25a3e

Browse files
authored
Merge pull request #35563 from dotnet/main
2 parents a28b6ba + ab12963 commit 3e25a3e

File tree

13 files changed

+228
-86
lines changed

13 files changed

+228
-86
lines changed

aspnetcore/blazor/call-web-api.md

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Learn how to call a web API from Blazor apps.
55
monikerRange: '>= aspnetcore-3.1'
66
ms.author: wpickett
77
ms.custom: mvc
8-
ms.date: 05/30/2025
8+
ms.date: 06/03/2025
99
uid: blazor/call-web-api
1010
---
1111
# Call a web API from ASP.NET Core Blazor
@@ -124,6 +124,10 @@ To configure a production distributed cache provider, see <xref:performance/cach
124124
125125
For more information, see [Token cache serialization: Distributed caches](/entra/msal/dotnet/how-to/token-cache-serialization?tabs=msal#distributed-caches). However, the code examples shown don't apply to ASP.NET Core apps, which configure distributed caches via <xref:Microsoft.Extensions.DependencyInjection.MemoryCacheServiceCollectionExtensions.AddDistributedMemoryCache%2A>, not <xref:Microsoft.Identity.Web.TokenCacheExtensions.AddDistributedTokenCache%2A>.
126126
127+
<!-- DOC AUTHOR NOTE: The next part on using a shared DP key ring is also
128+
covered in the *BWA+Entra* security article. Mirror
129+
changes when updating this portion of content. -->
130+
127131
Use a shared Data Protection key ring in production so that instances of the app across servers in a web farm can decrypt tokens when <xref:Microsoft.Identity.Web.TokenCacheProviders.Distributed.MsalDistributedTokenCacheAdapterOptions.Encrypt%2A?displayProperty=nameWithType> is set to `true`.
128132
129133
> [!NOTE]
@@ -135,22 +139,73 @@ Use a shared Data Protection key ring in production so that instances of the app
135139
>
136140
> Later in the development and testing period, enable token encryption and adopt a shared Data Protection key ring.
137141
138-
The following example shows how to use [Azure Blob Storage and Azure Key Vault](xref:security/data-protection/configuration/overview#protectkeyswithazurekeyvault) for the shared key ring. Add the following packages to the server project of the Blazor Web App:
142+
The following example shows how to use [Azure Blob Storage and Azure Key Vault (`PersistKeysToAzureBlobStorage`/`ProtectKeysWithAzureKeyVault`)](xref:security/data-protection/configuration/overview#protectkeyswithazurekeyvault) for the shared key ring. The service configurations are base case scenarios for demonstration purposes. Before deploying production apps, familiarize yourself with the Azure services and adopt best practices using their dedicated documentation sets, which are listed at the end of this section.
143+
144+
Add the following packages to the server project of the Blazor Web App:
139145
140146
* [`Azure.Extensions.AspNetCore.DataProtection.Blobs`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Blobs)
141147
* [`Azure.Extensions.AspNetCore.DataProtection.Keys`](https://www.nuget.org/packages/Azure.Extensions.AspNetCore.DataProtection.Keys)
142148
143149
[!INCLUDE[](~/includes/package-reference.md)]
144150
145-
Configure Azure Blob Storage to maintain the encrypted keys and protect them with Azure Key Vault. In the following example, the `{BLOB URI WITH SAS TOKEN}` placeholder is the full URI where the key file should be stored with the SAS token as a query string parameter, and the `{KEY IDENTIFIER}` placeholder is the key vault key identifier used for key encryption:
151+
> [!NOTE]
152+
> Before proceeding with the following steps, confirm that the app is registered with Microsoft Entra.
153+
154+
Configure Azure Blob Storage to maintain Data Protection keys and encrypt them at rest with Azure Key Vault:
155+
156+
* Create an Azure storage account. The account name in the following example is `contoso`.
157+
158+
* Create a container to hold the Data Protection keys. The container name in the following example is `data-protection`.
159+
160+
* Create the key file on your local machine. In the following example, the key file is named `keys.xml`. You can use a text editor to create the file.
161+
162+
`keys.xml`:
163+
164+
```xml
165+
<?xml version="1.0" encoding="utf-8"?>
166+
<repository>
167+
</repository>
168+
```
169+
170+
* Upload the key file (`keys.xml`) to the container of the storage account. Use the context menu's **View/edit** command at the end of the key row in the portal to confirm that the blob contains the preceding content.
171+
172+
* Use the context menu's **Generate SAS** command to obtain the blob's URI with a shared access signature (SAS). When you create the SAS, use the following permissions: `Read`, `Add`, `Create`, `Write`, `Delete`. The URI is used later where the `{BLOB URI WITH SAS}` placeholder appears.
173+
174+
When establishing the key vault in the Entra or Azure portal:
175+
176+
* Configure the key vault to use a **Vault access policy**. Confirm that public access on the **Networking** step is **enabled** (checked).
177+
178+
* In the **Access policies** pane, create a new access policy with `Get`, `Unwrap Key`, and `Wrap Key` Key permissions. Select the registered application as the service principal.
179+
180+
* When key encryption is active, keys in the key file include the comment, ":::no-loc text="This key is encrypted with Azure Key Vault.":::" After starting the app, select the **View/edit** command from the context menu at the end of the key row to confirm that a key is present with key vault security applied.
181+
182+
The <xref:Microsoft.Extensions.Azure.AzureEventSourceLogForwarder> service in the following example forwards log messages from Azure SDK for logging and requires the [`Microsoft.Extensions.Azure` NuGet package](https://www.nuget.org/packages/Microsoft.Extensions.Azure).
183+
184+
[!INCLUDE[](~/includes/package-reference.md)]
185+
186+
At the top of the `Program` file, provide access to the API in the <xref:Microsoft.Extensions.Azure?displayProperty=fullName> namespace:
187+
188+
```csharp
189+
using Microsoft.Extensions.Azure;
190+
```
191+
192+
Use the following code in the `Program` file where services are registered:
146193
147194
```csharp
195+
builder.Services.TryAddSingleton<AzureEventSourceLogForwarder>();
196+
148197
builder.Services.AddDataProtection()
149-
.PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS TOKEN}"))
198+
.PersistKeysToAzureBlobStorage(new Uri("{BLOB URI WITH SAS}"))
150199
.ProtectKeysWithAzureKeyVault(new Uri("{KEY IDENTIFIER}"), new DefaultAzureCredential());
151200
```
152201
153-
For more information on using a shared Data Protection key ring, see <xref:host-and-deploy/web-farm#data-protection> and <xref:security/data-protection/configuration/overview>.
202+
`{BLOB URI WITH SAS}`: The full URI where the key file should be stored with the SAS token as a query string parameter. The URI is generated by Azure Storage when you request a SAS for the uploaded key file. The container name in the following example is `data-protection`, and the storage account name is `contoso`. The key file is named `keys.xml`.
203+
204+
Example: :::no-loc text="https://contoso.blob.core.windows.net/data-protection/keys.xml?sp={PERMISSIONS}&st={START DATETIME}&se={EXPIRATION DATETIME}&spr=https&sv={STORAGE VERSION DATE}&sr=c&sig={TOKEN}":::
205+
206+
`{KEY IDENTIFIER}`: Azure Key Vault key identifier used for key encryption. The key vault name is `contoso` in the following example, and an access policy allows the application to access the key vault with `Get`, `Unwrap Key`, and `Wrap Key` permissions. The example key name is `data-protection`. The version of the key (`{KEY VERSION}` placeholder) is obtained from the key in the Entra or Azure portal after it's created.
207+
208+
Example: :::no-loc text="https://contoso.vault.azure.net/keys/data-protection/{KEY VERSION}":::
154209
155210
Inject <xref:Microsoft.Identity.Abstractions.IDownstreamApi> and call <xref:Microsoft.Identity.Abstractions.IDownstreamApi.CallApiForUserAsync%2A> when calling on behalf of a user:
156211
@@ -180,6 +235,11 @@ For more information, see the following resources:
180235
* *Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID*
181236
* [Non-BFF pattern (Interactive Auto)](xref:blazor/security/blazor-web-app-entra?pivots=non-bff-pattern)
182237
* [BFF pattern (Interactive Auto)](xref:blazor/security/blazor-web-app-entra?pivots=non-bff-pattern-server)
238+
* [Host ASP.NET Core in a web farm: Data Protection](xref:host-and-deploy/web-farm#data-protection)
239+
* <xref:security/data-protection/configuration/overview>
240+
* <xref:security/data-protection/implementation/key-storage-providers>
241+
* [Azure Key Vault documentation](/azure/key-vault/general/)
242+
* [Azure Storage documentation](/azure/storage/)
183243
184244
## Sample apps
185245
@@ -304,6 +364,27 @@ The solution includes a demonstration of obtaining weather data securely via an
304364
305365
:::moniker-end
306366
367+
## Disposal of `HttpRequestMessage`, `HttpResponseMessage`, and `HttpClient`
368+
369+
An <xref:System.Net.Http.HttpRequestMessage> without a body doesn't require explicit disposal with a [`using` declaration (C# 8 or later)](/dotnet/csharp/language-reference/proposals/csharp-8.0/using) or a `using` block (earlier than C# 8), but we recommend disposing with every use for the following reasons:
370+
371+
* It provides a performance improvement by avoiding finalizers.
372+
* It hardens the code for the future in case a request body is ever added to a <xref:System.Net.Http.HttpRequestMessage> that didn't initially have one.
373+
* It potentially avoids functional issues if a delegating handler expects <xref:System.IDisposable.Dispose%2A>/<xref:System.IAsyncDisposable.DisposeAsync%2A> to be called.
374+
* It's simpler to apply a general rule everywhere than trying to remember the specific cases when it matters.
375+
376+
Always dispose of <xref:System.Net.Http.HttpResponseMessage> instances.
377+
378+
***Never*** dispose of <xref:System.Net.Http.HttpClient> instances created by calling <xref:System.Net.Http.IHttpClientFactory.CreateClient%2A> because they're managed by the framework.
379+
380+
Example:
381+
382+
```csharp
383+
using var request = new HttpRequestMessage(HttpMethod.Get, "/weather-forecast");
384+
var client = clientFactory.CreateClient("ExternalApi");
385+
using var response = await client.SendAsync(request);
386+
```
387+
307388
## Client-side scenarios for calling external web APIs
308389
309390
Client-based components call external web APIs using <xref:System.Net.Http.HttpClient> instances, typically created with a preconfigured <xref:System.Net.Http.HttpClient> registered in the `Program` file:
@@ -351,12 +432,12 @@ else
351432
352433
protected override async Task OnInitializedAsync()
353434
{
354-
var request = new HttpRequestMessage(HttpMethod.Get,
435+
using var request = new HttpRequestMessage(HttpMethod.Get,
355436
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
356437
request.Headers.Add("Accept", "application/vnd.github.v3+json");
357438
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
358439
359-
var response = await Client.SendAsync(request);
440+
using var response = await Client.SendAsync(request);
360441
361442
if (response.IsSuccessStatusCode)
362443
{
@@ -411,10 +492,10 @@ Even if you call <xref:Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssem
411492
412493
var urlEncodedRequestUri = WebUtility.UrlEncode("{REQUEST URI}");
413494
414-
var request = new HttpRequestMessage(HttpMethod.Get,
495+
using var request = new HttpRequestMessage(HttpMethod.Get,
415496
$"https://corsproxy.io/?{urlEncodedRequestUri}");
416497
417-
var response = await client.SendAsync(request);
498+
using var response = await client.SendAsync(request);
418499
419500
...
420501
}
@@ -468,14 +549,14 @@ else
468549
469550
protected override async Task OnInitializedAsync()
470551
{
471-
var request = new HttpRequestMessage(HttpMethod.Get,
552+
using var request = new HttpRequestMessage(HttpMethod.Get,
472553
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
473554
request.Headers.Add("Accept", "application/vnd.github.v3+json");
474555
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
475556
476557
var client = ClientFactory.CreateClient();
477558
478-
var response = await client.SendAsync(request);
559+
using var response = await client.SendAsync(request);
479560
480561
if (response.IsSuccessStatusCode)
481562
{
@@ -579,11 +660,11 @@ In the following file upload example:
579660
* `Http` is the <xref:System.Net.Http.HttpClient>.
580661
581662
```csharp
582-
var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
663+
using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
583664
request.SetBrowserRequestStreamingEnabled(true);
584665
request.Content = content;
585666
586-
var response = await Http.SendAsync(request);
667+
using var response = await Http.SendAsync(request);
587668
```
588669
589670
Streaming requests:
@@ -737,7 +818,7 @@ As of C# 11 (.NET 7), you can compose a JSON string as a [raw string literal](/d
737818
<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:
738819
739820
```csharp
740-
var response = await Http.PatchAsJsonAsync(...);
821+
using var response = await Http.PatchAsJsonAsync(...);
741822
var content = await response.Content.ReadFromJsonAsync<TodoItem[]>() ??
742823
Array.Empty<TodoItem>();
743824
```
@@ -1120,7 +1201,7 @@ For a demonstration, see <xref:blazor/security/webassembly/standalone-with-ident
11201201
When composing an <xref:System.Net.Http.HttpRequestMessage>, set the browser request credentials and header directly:
11211202
11221203
```csharp
1123-
var request = new HttpRequestMessage() { ... };
1204+
using var request = new HttpRequestMessage() { ... };
11241205
11251206
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
11261207
request.Headers.Add("X-Requested-With", [ "XMLHttpRequest" ]);
@@ -1156,7 +1237,7 @@ request.Headers.Add("X-Requested-With", [ "XMLHttpRequest" ]);
11561237
11571238
private async Task PostRequest()
11581239
{
1159-
var request = new HttpRequestMessage()
1240+
using var request = new HttpRequestMessage()
11601241
{
11611242
Method = new HttpMethod("POST"),
11621243
RequestUri = new Uri("https://localhost:10000/todoitems"),
@@ -1178,7 +1259,7 @@ request.Headers.Add("X-Requested-With", [ "XMLHttpRequest" ]);
11781259
request.Content.Headers.TryAddWithoutValidation(
11791260
"x-custom-header", "value");
11801261
1181-
var response = await Http.SendAsync(request);
1262+
using var response = await Http.SendAsync(request);
11821263
var responseStatusCode = response.StatusCode;
11831264
11841265
responseBody = await response.Content.ReadAsStringAsync();
@@ -1241,8 +1322,9 @@ request.SetBrowserResponseStreamingEnabled(true);
12411322
By default, [`HttpCompletionOption.ResponseContentRead`](xref:System.Net.Http.HttpCompletionOption) is set, which results in the <xref:System.Net.Http.HttpClient> completing after reading the entire response, including the content. In order to be able to use the <xref:Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions.SetBrowserResponseStreamingEnabled%2A> option on large files, set [`HttpCompletionOption.ResponseHeadersRead`](xref:System.Net.Http.HttpCompletionOption) to avoid caching the file's content in memory:
12421323
12431324
```diff
1244-
- var response = await Http.SendAsync(request);
1245-
+ var response = await Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
1325+
- using var response = await Http.SendAsync(request);
1326+
+ using var response = await Http.SendAsync(request,
1327+
+ HttpCompletionOption.ResponseHeadersRead);
12461328
```
12471329
12481330
:::moniker-end
@@ -1345,9 +1427,9 @@ To add antiforgery support to an HTTP request, inject the `AntiforgeryStateProvi
13451427
private async Task OnSubmit()
13461428
{
13471429
var antiforgery = Antiforgery.GetAntiforgeryToken();
1348-
var request = new HttpRequestMessage(HttpMethod.Post, "action");
1430+
using var request = new HttpRequestMessage(HttpMethod.Post, "action");
13491431
request.Headers.Add("RequestVerificationToken", antiforgery.RequestToken);
1350-
var response = await client.SendAsync(request);
1432+
using var response = await client.SendAsync(request);
13511433
...
13521434
}
13531435
```

aspnetcore/blazor/components/quickgrid.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ The <xref:Microsoft.AspNetCore.Components.QuickGrid.GridItemsProvider%601> conve
446446
{ "limit", req.Count },
447447
});
448448
449-
var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(
449+
using var response = await Http.GetFromJsonAsync<FoodRecallQueryResult>(
450450
url, req.CancellationToken);
451451
452452
return GridItemsProviderResult.From(

aspnetcore/blazor/file-uploads.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -709,11 +709,11 @@ In a Blazor Web App, add the <xref:Microsoft.AspNetCore.Components.WebAssembly.H
709709
710710
if (upload)
711711
{
712-
var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
712+
using var request = new HttpRequestMessage(HttpMethod.Post, "/Filesave");
713713
request.SetBrowserRequestStreamingEnabled(true);
714714
request.Content = content;
715715
716-
var response = await Http.SendAsync(request);
716+
using var response = await Http.SendAsync(request);
717717
718718
var newUploadResults = await response.Content
719719
.ReadFromJsonAsync<IList<UploadResult>>();

aspnetcore/blazor/forms/validation.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,7 @@ In the following component, update the namespace of the **`Shared`** project (`@
744744
745745
try
746746
{
747-
var response = await Http.PostAsJsonAsync<Starship>(
747+
using var response = await Http.PostAsJsonAsync<Starship>(
748748
"StarshipValidation", (Starship)editContext.Model);
749749
750750
var errors = await response.Content
@@ -888,7 +888,7 @@ The preceding example sets the base address with `builder.HostEnvironment.BaseAd
888888
889889
try
890890
{
891-
var response = await Http.PostAsJsonAsync<Starship>(
891+
using var response = await Http.PostAsJsonAsync<Starship>(
892892
"StarshipValidation", (Starship)editContext.Model);
893893
894894
var errors = await response.Content

aspnetcore/blazor/hybrid/security/maui-blazor-web-identity.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ In `MauiBlazorWeb/Services/MauiAuthenticationStateProvider.cs`:
162162
// Call the Login endpoint and pass the email and password
163163
var httpClient = HttpClientHelper.GetHttpClient();
164164
var loginData = new { loginModel.Email, loginModel.Password };
165-
var response = await httpClient.PostAsJsonAsync(HttpClientHelper.LoginUrl,
165+
using var response = await httpClient.PostAsJsonAsync(HttpClientHelper.LoginUrl,
166166
loginData);
167167

168168
LoginStatus =

0 commit comments

Comments
 (0)