From ade268a82a37781ad27f50439695c710e3a91391 Mon Sep 17 00:00:00 2001 From: Tim Deschryver <28659384+timdeschryver@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:07:52 +0200 Subject: [PATCH 1/8] Adds .NET 9 sample to mongo app (#35189) * copy-paste to 9.x * update code to .NET 9 * update references to code * update references to 8.x code * upate ms date * add missing snapshot * Update aspnetcore/tutorials/first-mongo-app.md --------- Co-authored-by: Wade Pickett --- aspnetcore/tutorials/first-mongo-app.md | 32 ++++----- .../includes/first-mongo-app8.md | 26 +++---- .../9.x/BookStoreApi/BookStoreApi.csproj | 15 ++++ .../Controllers/BooksController.cs | 72 +++++++++++++++++++ .../Controllers/WeatherForecastController.cs | 32 +++++++++ .../samples/9.x/BookStoreApi/Models/Book.cs | 26 +++++++ .../Models/BookStoreDatabaseSettings.cs | 10 +++ .../samples/9.x/BookStoreApi/Program.cs | 46 ++++++++++++ .../9.x/BookStoreApi/Services/BooksService.cs | 42 +++++++++++ .../9.x/BookStoreApi/WeatherForecast.cs | 12 ++++ .../BookStoreApi/appsettings.Development.json | 8 +++ .../samples/9.x/BookStoreApi/appsettings.json | 14 ++++ .../samples_snapshot/9.x/Book.cs | 20 ++++++ 13 files changed, 326 insertions(+), 29 deletions(-) create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/BookStoreApi.csproj create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/BooksController.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/WeatherForecastController.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Program.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/WeatherForecast.cs create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.Development.json create mode 100644 aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.json create mode 100644 aspnetcore/tutorials/first-mongo-app/samples_snapshot/9.x/Book.cs diff --git a/aspnetcore/tutorials/first-mongo-app.md b/aspnetcore/tutorials/first-mongo-app.md index f8111ffba171..826e8a963517 100644 --- a/aspnetcore/tutorials/first-mongo-app.md +++ b/aspnetcore/tutorials/first-mongo-app.md @@ -5,8 +5,8 @@ author: wadepickett description: This tutorial demonstrates how to create an ASP.NET Core web API using a MongoDB NoSQL database. monikerRange: '>= aspnetcore-3.1' ms.author: wpickett -ms.custom: mvc, engagement-fy23 -ms.date: 04/17/2024 +ms.custom: mvc +ms.date: 04/09/2025 uid: tutorials/first-mongo-app --- # Create a web API with ASP.NET Core and MongoDB @@ -182,7 +182,7 @@ Use the previously installed MongoDB Shell in the following steps to create a da 1. Add a *Models* directory to the project root. 1. Add a `Book` class to the *Models* directory with the following code: - :::code language="csharp" source="first-mongo-app/samples_snapshot/6.x/Book.cs"::: + :::code language="csharp" source="first-mongo-app/samples_snapshot/9.x/Book.cs"::: In the preceding class, the `Id` property is: @@ -196,48 +196,48 @@ Use the previously installed MongoDB Shell in the following steps to create a da 1. Add the following database configuration values to `appsettings.json`: - :::code language="json" source="first-mongo-app/samples/6.x/BookStoreApi/appsettings.json" highlight="2-6"::: + :::code language="json" source="first-mongo-app/samples/9.x/BookStoreApi/appsettings.json" highlight="2-6"::: 1. Add a `BookStoreDatabaseSettings` class to the *Models* directory with the following code: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs"::: The preceding `BookStoreDatabaseSettings` class is used to store the `appsettings.json` file's `BookStoreDatabase` property values. The JSON and C# property names are named identically to ease the mapping process. 1. Add the following highlighted code to `Program.cs`: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_BookStoreDatabaseSettings" highlight="4-5"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Program.cs" id="snippet_BookStoreDatabaseSettings" highlight="4-5"::: In the preceding code, the configuration instance to which the `appsettings.json` file's `BookStoreDatabase` section binds is registered in the Dependency Injection (DI) container. For example, the `BookStoreDatabaseSettings` object's `ConnectionString` property is populated with the `BookStoreDatabase:ConnectionString` property in `appsettings.json`. 1. Add the following code to the top of `Program.cs` to resolve the `BookStoreDatabaseSettings` reference: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_UsingModels"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Program.cs" id="snippet_UsingModels"::: ## Add a CRUD operations service 1. Add a *Services* directory to the project root. 1. Add a `BooksService` class to the *Services* directory with the following code: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Services/BooksService.cs" id="snippet_File"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs" id="snippet_File"::: In the preceding code, a `BookStoreDatabaseSettings` instance is retrieved from DI via constructor injection. This technique provides access to the `appsettings.json` configuration values that were added in the [Add a configuration model](#add-a-configuration-model) section. 1. Add the following highlighted code to `Program.cs`: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_BooksService" highlight="7"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Program.cs" id="snippet_BooksService" highlight="7"::: In the preceding code, the `BooksService` class is registered with DI to support constructor injection in consuming classes. The singleton service lifetime is most appropriate because `BooksService` takes a direct dependency on `MongoClient`. Per the official [Mongo Client reuse guidelines](https://mongodb.github.io/mongo-csharp-driver/2.14/reference/driver/connecting/#re-use), `MongoClient` should be registered in DI with a singleton service lifetime. 1. Add the following code to the top of `Program.cs` to resolve the `BooksService` reference: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_UsingServices"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Program.cs" id="snippet_UsingServices"::: The `BooksService` class uses the following `MongoDB.Driver` members to run CRUD operations against the database: * [MongoClient](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/T_MongoDB_Driver_MongoClient.htm): Reads the server instance for running database operations. The constructor of this class is provided in the MongoDB connection string: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Services/BooksService.cs" id="snippet_ctor" highlight="4-5"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs" id="snippet_ctor" highlight="4-5"::: * [IMongoDatabase](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/T_MongoDB_Driver_IMongoDatabase.htm): Represents the Mongo database for running operations. This tutorial uses the generic [GetCollection\(collection)](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/M_MongoDB_Driver_IMongoDatabase_GetCollection__1.htm) method on the interface to gain access to data in a specific collection. Run CRUD operations against the collection after this method is called. In the `GetCollection(collection)` method call: @@ -255,7 +255,7 @@ The `BooksService` class uses the following `MongoDB.Driver` members to run CRUD Add a `BooksController` class to the *Controllers* directory with the following code: -:::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Controllers/BooksController.cs"::: +:::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Controllers/BooksController.cs"::: The preceding web API controller: @@ -311,19 +311,19 @@ To satisfy the preceding requirements, make the following changes: 1. In `Program.cs`, chain the following highlighted code on to the `AddControllers` method call: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_AddControllers" highlight="10-11"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Program.cs" id="snippet_AddControllers" highlight="10-11"::: With the preceding change, property names in the web API's serialized JSON response match their corresponding property names in the CLR object type. For example, the `Book` class's `Author` property serializes as `Author` instead of `author`. 1. In `Models/Book.cs`, annotate the `BookName` property with the [`[JsonPropertyName]`](xref:System.Text.Json.Serialization.JsonPropertyNameAttribute) attribute: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Models/Book.cs" id="snippet_BookName" highlight="2"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs" id="snippet_BookName" highlight="2"::: The `[JsonPropertyName]` attribute's value of `Name` represents the property name in the web API's serialized JSON response. 1. Add the following code to the top of `Models/Book.cs` to resolve the `[JsonProperty]` attribute reference: - :::code language="csharp" source="first-mongo-app/samples/6.x/BookStoreApi/Models/Book.cs" id="snippet_UsingSystemTextJsonSerialization"::: + :::code language="csharp" source="first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs" id="snippet_UsingSystemTextJsonSerialization"::: 1. Repeat the steps defined in the [Test the web API](#test-the-web-api) section. Notice the difference in JSON property names. @@ -333,7 +333,7 @@ To satisfy the preceding requirements, make the following changes: ## Additional resources -* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mongo-app/samples/8.x/BookStoreApi) ([how to download](xref:index#how-to-download-a-sample)) +* [View or download sample code](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi) ([how to download](xref:index#how-to-download-a-sample)) * * * [Create a web API with ASP.NET Core](/training/modules/build-web-api-aspnet-core/) diff --git a/aspnetcore/tutorials/first-mongo-app/includes/first-mongo-app8.md b/aspnetcore/tutorials/first-mongo-app/includes/first-mongo-app8.md index 6af18426df2b..b68145e8734b 100644 --- a/aspnetcore/tutorials/first-mongo-app/includes/first-mongo-app8.md +++ b/aspnetcore/tutorials/first-mongo-app/includes/first-mongo-app8.md @@ -175,7 +175,7 @@ Use the previously installed MongoDB Shell in the following steps to create a da 1. Add a *Models* directory to the project root. 1. Add a `Book` class to the *Models* directory with the following code: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples_snapshot/6.x/Book.cs"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples_snapshot/8.x/Book.cs"::: In the preceding class, the `Id` property is: @@ -189,48 +189,48 @@ Use the previously installed MongoDB Shell in the following steps to create a da 1. Add the following database configuration values to `appsettings.json`: - :::code language="json" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/appsettings.json" highlight="2-6"::: + :::code language="json" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/appsettings.json" highlight="2-6"::: 1. Add a `BookStoreDatabaseSettings` class to the *Models* directory with the following code: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs"::: The preceding `BookStoreDatabaseSettings` class is used to store the `appsettings.json` file's `BookStoreDatabase` property values. The JSON and C# property names are named identically to ease the mapping process. 1. Add the following highlighted code to `Program.cs`: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_BookStoreDatabaseSettings" highlight="4-5"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Program.cs" id="snippet_BookStoreDatabaseSettings" highlight="4-5"::: In the preceding code, the configuration instance to which the `appsettings.json` file's `BookStoreDatabase` section binds is registered in the Dependency Injection (DI) container. For example, the `BookStoreDatabaseSettings` object's `ConnectionString` property is populated with the `BookStoreDatabase:ConnectionString` property in `appsettings.json`. 1. Add the following code to the top of `Program.cs` to resolve the `BookStoreDatabaseSettings` reference: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_UsingModels"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Program.cs" id="snippet_UsingModels"::: ## Add a CRUD operations service 1. Add a *Services* directory to the project root. 1. Add a `BooksService` class to the *Services* directory with the following code: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Services/BooksService.cs" id="snippet_File"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Services/BooksService.cs" id="snippet_File"::: In the preceding code, a `BookStoreDatabaseSettings` instance is retrieved from DI via constructor injection. This technique provides access to the `appsettings.json` configuration values that were added in the [Add a configuration model](#add-a-configuration-model) section. 1. Add the following highlighted code to `Program.cs`: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_BooksService" highlight="7"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Program.cs" id="snippet_BooksService" highlight="7"::: In the preceding code, the `BooksService` class is registered with DI to support constructor injection in consuming classes. The singleton service lifetime is most appropriate because `BooksService` takes a direct dependency on `MongoClient`. Per the official [Mongo Client reuse guidelines](https://mongodb.github.io/mongo-csharp-driver/2.14/reference/driver/connecting/#re-use), `MongoClient` should be registered in DI with a singleton service lifetime. 1. Add the following code to the top of `Program.cs` to resolve the `BooksService` reference: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_UsingServices"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Program.cs" id="snippet_UsingServices"::: The `BooksService` class uses the following `MongoDB.Driver` members to run CRUD operations against the database: * [MongoClient](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/T_MongoDB_Driver_MongoClient.htm): Reads the server instance for running database operations. The constructor of this class is provided in the MongoDB connection string: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Services/BooksService.cs" id="snippet_ctor" highlight="4-5"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Services/BooksService.cs" id="snippet_ctor" highlight="4-5"::: * [IMongoDatabase](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/T_MongoDB_Driver_IMongoDatabase.htm): Represents the Mongo database for running operations. This tutorial uses the generic [GetCollection\(collection)](https://mongodb.github.io/mongo-csharp-driver/2.14/apidocs/html/M_MongoDB_Driver_IMongoDatabase_GetCollection__1.htm) method on the interface to gain access to data in a specific collection. Run CRUD operations against the collection after this method is called. In the `GetCollection(collection)` method call: @@ -248,7 +248,7 @@ The `BooksService` class uses the following `MongoDB.Driver` members to run CRUD Add a `BooksController` class to the *Controllers* directory with the following code: -:::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Controllers/BooksController.cs"::: +:::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Controllers/BooksController.cs"::: The preceding web API controller: @@ -304,19 +304,19 @@ To satisfy the preceding requirements, make the following changes: 1. In `Program.cs`, chain the following highlighted code on to the `AddControllers` method call: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Program.cs" id="snippet_AddControllers" highlight="10-11"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Program.cs" id="snippet_AddControllers" highlight="10-11"::: With the preceding change, property names in the web API's serialized JSON response match their corresponding property names in the CLR object type. For example, the `Book` class's `Author` property serializes as `Author` instead of `author`. 1. In `Models/Book.cs`, annotate the `BookName` property with the [`[JsonPropertyName]`](xref:System.Text.Json.Serialization.JsonPropertyNameAttribute) attribute: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Models/Book.cs" id="snippet_BookName" highlight="2"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Models/Book.cs" id="snippet_BookName" highlight="2"::: The `[JsonPropertyName]` attribute's value of `Name` represents the property name in the web API's serialized JSON response. 1. Add the following code to the top of `Models/Book.cs` to resolve the `[JsonProperty]` attribute reference: - :::code language="csharp" source="~/tutorials/first-mongo-app/samples/6.x/BookStoreApi/Models/Book.cs" id="snippet_UsingSystemTextJsonSerialization"::: + :::code language="csharp" source="~/tutorials/first-mongo-app/samples/8.x/BookStoreApi/Models/Book.cs" id="snippet_UsingSystemTextJsonSerialization"::: 1. Repeat the steps defined in the [Test the web API](#test-the-web-api) section. Notice the difference in JSON property names. diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/BookStoreApi.csproj b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/BookStoreApi.csproj new file mode 100644 index 000000000000..2e03412c3803 --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/BookStoreApi.csproj @@ -0,0 +1,15 @@ + + + + net9.0 + enable + enable + + + + + + + + + diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/BooksController.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/BooksController.cs new file mode 100644 index 000000000000..a163070b293f --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/BooksController.cs @@ -0,0 +1,72 @@ +using BookStoreApi.Models; +using BookStoreApi.Services; +using Microsoft.AspNetCore.Mvc; + +namespace BookStoreApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class BooksController : ControllerBase +{ + private readonly BooksService _booksService; + + public BooksController(BooksService booksService) => + _booksService = booksService; + + [HttpGet] + public async Task> Get() => + await _booksService.GetAsync(); + + [HttpGet("{id:length(24)}")] + public async Task> Get(string id) + { + var book = await _booksService.GetAsync(id); + + if (book is null) + { + return NotFound(); + } + + return book; + } + + [HttpPost] + public async Task Post(Book newBook) + { + await _booksService.CreateAsync(newBook); + + return CreatedAtAction(nameof(Get), new { id = newBook.Id }, newBook); + } + + [HttpPut("{id:length(24)}")] + public async Task Update(string id, Book updatedBook) + { + var book = await _booksService.GetAsync(id); + + if (book is null) + { + return NotFound(); + } + + updatedBook.Id = book.Id; + + await _booksService.UpdateAsync(id, updatedBook); + + return NoContent(); + } + + [HttpDelete("{id:length(24)}")] + public async Task Delete(string id) + { + var book = await _booksService.GetAsync(id); + + if (book is null) + { + return NotFound(); + } + + await _booksService.RemoveAsync(id); + + return NoContent(); + } +} \ No newline at end of file diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/WeatherForecastController.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000000..c06604cb988a --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace BookStoreApi.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} \ No newline at end of file diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs new file mode 100644 index 000000000000..93f1a7ab4d3a --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/Book.cs @@ -0,0 +1,26 @@ +// +using System.Text.Json.Serialization; +// +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace BookStoreApi.Models; + +public class Book +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + // + [BsonElement("Name")] + [JsonPropertyName("Name")] + public string BookName { get; set; } = null!; + // + + public decimal Price { get; set; } + + public string Category { get; set; } = null!; + + public string Author { get; set; } = null!; +} \ No newline at end of file diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs new file mode 100644 index 000000000000..edfe7ebb6054 --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Models/BookStoreDatabaseSettings.cs @@ -0,0 +1,10 @@ +namespace BookStoreApi.Models; + +public class BookStoreDatabaseSettings +{ + public string ConnectionString { get; set; } = null!; + + public string DatabaseName { get; set; } = null!; + + public string BooksCollectionName { get; set; } = null!; +} \ No newline at end of file diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Program.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Program.cs new file mode 100644 index 000000000000..75fb976c8077 --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Program.cs @@ -0,0 +1,46 @@ +// +using BookStoreApi.Models; +// +// +using BookStoreApi.Services; +// + +// +// +// +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.Configure( + builder.Configuration.GetSection("BookStoreDatabase")); +// + +builder.Services.AddSingleton(); +// + +builder.Services.AddControllers() + .AddJsonOptions( + options => options.JsonSerializerOptions.PropertyNamingPolicy = null); +// + +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/openapi/v1.json", "v1"); + }); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs new file mode 100644 index 000000000000..3565c8b43606 --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/Services/BooksService.cs @@ -0,0 +1,42 @@ +// +using BookStoreApi.Models; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace BookStoreApi.Services; + +public class BooksService +{ + private readonly IMongoCollection _booksCollection; + + // + public BooksService( + IOptions bookStoreDatabaseSettings) + { + var mongoClient = new MongoClient( + bookStoreDatabaseSettings.Value.ConnectionString); + + var mongoDatabase = mongoClient.GetDatabase( + bookStoreDatabaseSettings.Value.DatabaseName); + + _booksCollection = mongoDatabase.GetCollection( + bookStoreDatabaseSettings.Value.BooksCollectionName); + } + // + + public async Task> GetAsync() => + await _booksCollection.Find(_ => true).ToListAsync(); + + public async Task GetAsync(string id) => + await _booksCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(Book newBook) => + await _booksCollection.InsertOneAsync(newBook); + + public async Task UpdateAsync(string id, Book updatedBook) => + await _booksCollection.ReplaceOneAsync(x => x.Id == id, updatedBook); + + public async Task RemoveAsync(string id) => + await _booksCollection.DeleteOneAsync(x => x.Id == id); +} +// diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/WeatherForecast.cs b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/WeatherForecast.cs new file mode 100644 index 000000000000..840bfce93bf5 --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace BookStoreApi; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.Development.json b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.Development.json new file mode 100644 index 000000000000..0c208ae9181e --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.json b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.json new file mode 100644 index 000000000000..db0fa877639d --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples/9.x/BookStoreApi/appsettings.json @@ -0,0 +1,14 @@ +{ + "BookStoreDatabase": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "BookStore", + "BooksCollectionName": "Books" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/aspnetcore/tutorials/first-mongo-app/samples_snapshot/9.x/Book.cs b/aspnetcore/tutorials/first-mongo-app/samples_snapshot/9.x/Book.cs new file mode 100644 index 000000000000..97756d7d5d5a --- /dev/null +++ b/aspnetcore/tutorials/first-mongo-app/samples_snapshot/9.x/Book.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace BookStoreApi.Models; + +public class Book +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + + [BsonElement("Name")] + public string BookName { get; set; } = null!; + + public decimal Price { get; set; } + + public string Category { get; set; } = null!; + + public string Author { get; set; } = null!; +} From 45a33f8fdef25f1e51bc1b81cc11525343c272ba Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:31:45 -1000 Subject: [PATCH 2/8] SSE return types /2 (#35152) * SSE return types /2 * SSE return types /2 * SSE return types /2 * SSE return types /2 * SSE return types /2 * fixes * Update aspnetcore/fundamentals/minimal-apis/responses.md Co-authored-by: Mike Kistler * Apply suggestions from code review Co-authored-by: Mike Kistler * react to feedback * Update aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs Co-authored-by: Mike Kistler * react to feedback --------- Co-authored-by: Mike Kistler --- .../HeartRateRecord.cs | 4 ++ .../MinimalServerSentEvents.http | 4 +- .../MinimalServerSentEvents/Program.cs | 16 ++--- .../fundamentals/minimal-apis/responses.md | 16 ++++- aspnetcore/release-notes/aspnetcore-10.0.md | 2 + .../aspnetcore-10/includes/sse.md | 17 +++++ .../10/ControllerSSE/ControllerSSE.csproj | 13 ++++ .../10/ControllerSSE/ControllerSSE.http | 15 ++++ .../Controllers/HeartRateController.cs | 68 +++++++++++++++++++ .../samples/10/ControllerSSE/HearRate.cs | 4 ++ .../samples/10/ControllerSSE/Program.cs | 28 ++++++++ 11 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs create mode 100644 aspnetcore/release-notes/aspnetcore-10/includes/sse.md create mode 100644 aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj create mode 100644 aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http create mode 100644 aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs create mode 100644 aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs create mode 100644 aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs new file mode 100644 index 000000000000..b9950900f083 --- /dev/null +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/HeartRateRecord.cs @@ -0,0 +1,4 @@ +public record HeartRateRecord(DateTime Timestamp, int HeartRate) +{ + public static HeartRateRecord Create(int heartRate) => new(DateTime.UtcNow, heartRate); +} diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http index 6e19822ba6a5..b61db15b6b30 100644 --- a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/MinimalServerSentEvents.http @@ -1,4 +1,4 @@ -@baseUrl = http://localhost:5293 +@baseUrl = http://localhost:58489 ### Connect to SSE stream # This request will open an SSE connection that stays open @@ -12,4 +12,4 @@ Accept: text/event-stream ### GET {{baseUrl}}/sse-item -Accept: text/event-stream \ No newline at end of file +Accept: text/event-stream diff --git a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs index 42b873277604..d3354b692fc1 100644 --- a/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs +++ b/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs @@ -13,30 +13,32 @@ async IAsyncEnumerable GetHeartRate( while (!cancellationToken.IsCancellationRequested) { var heartRate = Random.Shared.Next(60, 100); - yield return $"Hear Rate: {heartRate} bpm"; + yield return $"Heart Rate: {heartRate} bpm"; await Task.Delay(2000, cancellationToken); } } - return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), + eventType: "heartRate"); }); // // app.MapGet("/json-item", (CancellationToken cancellationToken) => { - async IAsyncEnumerable GetHeartRate( + async IAsyncEnumerable GetHeartRate( [EnumeratorCancellation] CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { var heartRate = Random.Shared.Next(60, 100); - yield return HearRate.Create(heartRate); + yield return HeartRateRecord.Create(heartRate); await Task.Delay(2000, cancellationToken); } } - return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), + eventType: "heartRate"); }); // @@ -64,7 +66,3 @@ async IAsyncEnumerable> GetHeartRate( app.Run(); -public record HearRate(DateTime Timestamp, int HeartRate) -{ - public static HearRate Create(int heartRate) => new(DateTime.UtcNow, heartRate); -} diff --git a/aspnetcore/fundamentals/minimal-apis/responses.md b/aspnetcore/fundamentals/minimal-apis/responses.md index 0f6dca09bb47..71cb31cc663a 100644 --- a/aspnetcore/fundamentals/minimal-apis/responses.md +++ b/aspnetcore/fundamentals/minimal-apis/responses.md @@ -120,7 +120,7 @@ In order to document this endpoint correctly the extension method `Produces` is :::code language="csharp" source="~/fundamentals/minimal-apis/9.0-samples/Snippets/Program.cs" id="snippet_04"::: - + ### Built-in results @@ -172,6 +172,20 @@ The following example streams a video from an Azure Blob: [!code-csharp[](~/fundamentals/minimal-apis/resultsStream/7.0-samples/ResultsStreamSample/Program.cs?name=snippet_video)] +#### Server-Sent Events (SSE) + +The [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API supports returning a [ServerSentEvents](xref:System.Net.ServerSentEvents) result. + +[Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET, the event messages are represented as [`SseItem`](/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`. + +The [TypedResults](xref:Microsoft.AspNetCore.Http.TypedResults) class has a static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable>` that represents the stream of event messages to be sent to the client. + +The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client: + +:::code language="csharp" source="~/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs" id="snippet_item" ::: + +For more information, see the [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. + #### Redirect :::code language="csharp" source="~/fundamentals/minimal-apis/9.0-samples/Snippets/Program.cs" id="snippet_09"::: diff --git a/aspnetcore/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index 2976645208ee..b2c917984a78 100644 --- a/aspnetcore/release-notes/aspnetcore-10.0.md +++ b/aspnetcore/release-notes/aspnetcore-10.0.md @@ -37,6 +37,8 @@ This section describes new features for minimal APIs. [!INCLUDE[](~/release-notes/aspnetcore-10/includes/MinApiEmptyStringInFormPost.md)] +[!INCLUDE[](~/release-notes/aspnetcore-10/includes/sse.md)] + ## OpenAPI This section describes new features for OpenAPI. diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/sse.md b/aspnetcore/release-notes/aspnetcore-10/includes/sse.md new file mode 100644 index 000000000000..dd48c54d8340 --- /dev/null +++ b/aspnetcore/release-notes/aspnetcore-10/includes/sse.md @@ -0,0 +1,17 @@ +### Support for Server-Sent Events (SSE) + +ASP.NET Core now supports returning a [ServerSentEvents](xref:System.Net.ServerSentEvents) result using the [TypedResults.ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,051e6796e1492f84) API. This feature is supported in both Minimal APIs and controller-based apps. + +Server-Sent Events is a server push technology that allows a server to send a stream of event messages to a client over a single HTTP connection. In .NET the event messages are represented as [`SseItem`](/dotnet/api/system.net.serversentevents.sseitem-1) objects, which may contain an event type, an ID, and a data payload of type `T`. + +The [TypedResults](xref:Microsoft.AspNetCore.Http.TypedResults) class has a new static method called [ServerSentEvents](https://source.dot.net/#Microsoft.AspNetCore.Http.Results/TypedResults.cs,ceb980606eb9e295) that can be used to return a `ServerSentEvents` result. The first parameter to this method is an `IAsyncEnumerable>` that represents the stream of event messages to be sent to the client. + +The following example illustrates how to use the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as JSON objects to the client: + +:::code language="csharp" source="~/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs" id="snippet_json" ::: + +For more information, see: + +- [Server-Sent Events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) on MDN. +- [Minimal API sample app](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/fundamentals/minimal-apis/10.0-samples/MinimalServerSentEvents/Program.cs) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. +- [Controller API sample app](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE) using the `TypedResults.ServerSentEvents` API to return a stream of heart rate events as string, `ServerSentEvents`, and JSON objects to the client. diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj new file mode 100644 index 000000000000..b6fe0f1ed00c --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http new file mode 100644 index 000000000000..960c3051d23b --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/ControllerSSE.http @@ -0,0 +1,15 @@ +@baseUrl = http://localhost:5201/HeartRate + +### Connect to SSE stream +# This request will open an SSE connection that stays open +GET {{baseUrl}}/string-item +Accept: text/event-stream + +### +GET {{baseUrl}}/json-item +Accept: text/event-stream + +### + +GET {{baseUrl}}/sse-item +Accept: text/event-stream \ No newline at end of file diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs new file mode 100644 index 000000000000..f42f9a57c468 --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Controllers/HeartRateController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using System.Runtime.CompilerServices; +using System.Net.ServerSentEvents; + +[ApiController] +[Route("[controller]")] + +public class HeartRateController : ControllerBase +{ + // /HeartRate/json-item + [HttpGet("json-item")] + public IResult GetHeartRateJson(CancellationToken cancellationToken) + { + async IAsyncEnumerable StreamHeartRates( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return HearRate.Create(heartRate); + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(StreamHeartRates(cancellationToken), eventType: "heartRate"); + } + + // /HeartRate/string-item + [HttpGet("string-item")] + + public IResult GetHeartRateString(CancellationToken cancellationToken) + { + async IAsyncEnumerable GetHeartRate( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return $"Hear Rate: {heartRate} bpm"; + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken), eventType: "heartRate"); + } + + // /HeartRate/sse-item + [HttpGet("sse-item")] + + public IResult GetHeartRateSSE(CancellationToken cancellationToken) + { + async IAsyncEnumerable> GetHeartRate( + [EnumeratorCancellation] CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + var heartRate = Random.Shared.Next(60, 100); + yield return new SseItem(heartRate, eventType: "heartRate") + { + ReconnectionInterval = TimeSpan.FromMinutes(1) + }; + await Task.Delay(2000, cancellationToken); + } + } + + return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken)); + } +} diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs new file mode 100644 index 000000000000..c784313bc4ad --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/HearRate.cs @@ -0,0 +1,4 @@ +public record HeartRate(DateTime Timestamp, int HeartRate) +{ + public static HeartRate Create(int heartRate) => new(DateTime.UtcNow, heartRate); +} diff --git a/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs new file mode 100644 index 000000000000..08ee54a07da9 --- /dev/null +++ b/aspnetcore/web-api/action-return-types/samples/10/ControllerSSE/Program.cs @@ -0,0 +1,28 @@ + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi +builder.Services.AddOpenApi(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + + +app.MapControllers(); + +app.MapGet("/", () => Results.Redirect("/HeartRate/json-item")); + + +app.Run(); \ No newline at end of file From 5a02e0eac1c0ef9ac07bd2abd0627f81ab3a60a2 Mon Sep 17 00:00:00 2001 From: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:43:00 -1000 Subject: [PATCH 3/8] Update complex-data-model.md Fixes #35191 --- aspnetcore/data/ef-rp/complex-data-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aspnetcore/data/ef-rp/complex-data-model.md b/aspnetcore/data/ef-rp/complex-data-model.md index 643bf2cbe0f0..107aed3b852e 100644 --- a/aspnetcore/data/ef-rp/complex-data-model.md +++ b/aspnetcore/data/ef-rp/complex-data-model.md @@ -417,7 +417,7 @@ An enrollment record is for one course taken by one student. ![Enrollment entity](complex-data-model/_static/enrollment-entity.png) -Update `Models/Enrollment.cs` with the following code: +Review `Models/Enrollment.cs`: [!code-csharp[](intro/samples/cu30/Models/Enrollment.cs?highlight=1-2,16)] From 0a9980265358aa1b314809fffba31a74fc92df9d Mon Sep 17 00:00:00 2001 From: Wade Pickett Date: Wed, 9 Apr 2025 19:48:06 -0700 Subject: [PATCH 4/8] WN .NET 10 Prev 3: Validation Support Minimal API (#35188) * WN .NET 10 Prev 3: Validation Support Minimal API * Added include to What's New topic for .NET 10 Preview 3 * Update with correct links * Format link for attribute * Correct DataAnnotations link * Add review suggestions, remove future tense and lines * Minor edit * Remove line breaks * fixed line break --- aspnetcore/release-notes/aspnetcore-10.0.md | 2 ++ .../includes/ValidationSupportMinAPI.md | 30 +++++++++---------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/aspnetcore/release-notes/aspnetcore-10.0.md b/aspnetcore/release-notes/aspnetcore-10.0.md index b2c917984a78..4c674cb9f656 100644 --- a/aspnetcore/release-notes/aspnetcore-10.0.md +++ b/aspnetcore/release-notes/aspnetcore-10.0.md @@ -37,6 +37,8 @@ This section describes new features for minimal APIs. [!INCLUDE[](~/release-notes/aspnetcore-10/includes/MinApiEmptyStringInFormPost.md)] +[!INCLUDE[](~/release-notes/aspnetcore-10/includes/ValidationSupportMinAPI.md)] + [!INCLUDE[](~/release-notes/aspnetcore-10/includes/sse.md)] ## OpenAPI diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/ValidationSupportMinAPI.md b/aspnetcore/release-notes/aspnetcore-10/includes/ValidationSupportMinAPI.md index ba6f93bb33b8..85e9628fe60b 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/ValidationSupportMinAPI.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/ValidationSupportMinAPI.md @@ -1,33 +1,33 @@ ### Validation support in Minimal APIs - +Support for validation in Minimal APIs is now available. This feature allows you to request validation of data sent to your API endpoints. Enabling validation allows the ASP.NET Core runtime to perform any validations defined on the: -Support for validation in Minimal APIs is now available. This feature allows you to request validation of data -sent to your API endpoints. When validation is enabled, the ASP.NET Core runtime will perform any validations -defined on query, header, and route parameters, as well as on the request body. -Validations can be defined using attributes in the `System.ComponentModel.DataAnnotations` namespace. -Developers can customize the behavior of the validation system by: +* Query +* Header +* Request body -- creating custom [ValidationAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationattribute?view=net-9.0) implementations -- implement the [IValidatableObject](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject?view=net-9.0) interface for complex validation logic +Validations are defined using attributes in the [`DataAnnotations`](xref:System.ComponentModel.DataAnnotations) namespace. Developers customize the behavior of the validation system by: -When validation fails, the runtime will return a 400 Bad Request response with -details of the validation errors. +* Creating custom [`[Validation]`](xref:System.ComponentModel.DataAnnotations.ValidationAttribute) attribute implementations. +* Implementing the [`IValidatableObject`](xref:System.ComponentModel.DataAnnotations.IValidatableObject) interface for complex validation logic. -To enable built-in validation support for minimal APIs, call the `AddValidation` extension method to register -the required services into the service container for your application. +If validation fails, the runtime returns a 400 Bad Request response with details of the validation errors. + +#### Enable built-in validation support for minimal APIs + +Enable the built-in validation support for minimal APIs by calling the `AddValidation` extension method to register the required services in the service container for your application: ```csharp builder.Services.AddValidation(); ``` -The implementation automatically discovers types that are defined in minimal API handlers or as base types of types defined in minimal API handlers. Validation is then performed on these types by an endpoint filter that is added for each endpoint. +The implementation automatically discovers types that are defined in minimal API handlers or as base types of types defined in minimal API handlers. An endpoint filter performs validation on these types and is added for each endpoint. -Validation can be disabled for specific endpoints by using the `DisableValidation` extension method. +Validation can be disabled for specific endpoints by using the `DisableValidation` extension method, as in the following example: ```csharp app.MapPost("/products", ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name) => TypedResults.Ok(productId)) .DisableValidation(); -``` \ No newline at end of file +``` From 6a9696af0606feac8323fffef652924b5d20bda1 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 10 Apr 2025 09:35:33 -0400 Subject: [PATCH 5/8] [Pre3] Boot config file name change (#35176) --- .../blazor/fundamentals/static-files.md | 6 +- ...le-caching-and-integrity-check-failures.md | 35 +++++-- .../webassembly/github-pages.md | 8 +- .../host-and-deploy/webassembly/index.md | 91 +++++++++++++++++-- aspnetcore/blazor/progressive-web-app.md | 10 ++ .../webassembly-lazy-load-assemblies.md | 2 +- .../aspnetcore-10/includes/blazor.md | 7 ++ 7 files changed, 132 insertions(+), 27 deletions(-) diff --git a/aspnetcore/blazor/fundamentals/static-files.md b/aspnetcore/blazor/fundamentals/static-files.md index cf5234107fdf..2f9b19b57538 100644 --- a/aspnetcore/blazor/fundamentals/static-files.md +++ b/aspnetcore/blazor/fundamentals/static-files.md @@ -372,7 +372,7 @@ In the preceding example, the `{PATH}` placeholder is the path. Without setting the `` property, a standalone app is published at `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/`. -In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) (for example, `net6.0`). +In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks). If the `` property in a standalone Blazor WebAssembly app sets the published static asset path to `app1`, the root path to the app in published output is `/app1`. @@ -386,7 +386,7 @@ In the standalone Blazor WebAssembly app's project file (`.csproj`): In published output, the path to the standalone Blazor WebAssembly app is `/BlazorStandaloneSample/bin/Release/{TFM}/publish/wwwroot/app1/`. -In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) (for example, `net6.0`). +In the preceding example, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks). :::moniker-end @@ -426,7 +426,7 @@ In published output: The `` property is most commonly used to control the paths to published static assets of multiple Blazor WebAssembly apps in a single hosted deployment. For more information, see . The property is also effective in standalone Blazor WebAssembly apps. -In the preceding examples, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) (for example, `net6.0`). +In the preceding examples, the `{TFM}` placeholder is the [Target Framework Moniker (TFM)](/dotnet/standard/frameworks). :::moniker-end diff --git a/aspnetcore/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures.md b/aspnetcore/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures.md index 7be4b53dfe8a..1142d42602c4 100644 --- a/aspnetcore/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures.md +++ b/aspnetcore/blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures.md @@ -20,9 +20,9 @@ When a Blazor WebAssembly app loads in the browser, the app downloads boot resou * .NET runtime and assemblies * Locale specific data -Except for Blazor's boot resources file (`blazor.boot.json`), WebAssembly .NET runtime and app bundle files are cached on clients. The `blazor.boot.json` file contains a manifest of the files that make up the app that must be downloaded along with a hash of the file's content that's used to detect whether any of the boot resources have changed. Blazor caches downloaded files using the browser [Cache](https://developer.mozilla.org/docs/Web/API/Cache) API. +Except for Blazor's boot manifest file (`dotnet.boot.js` in .NET 10 or later, `blazor.boot.json` prior to .NET 10), WebAssembly .NET runtime and app bundle files are cached on clients. The Blazor boot configuration contains a manifest of the files that make up the app that must be downloaded along with a hash of the file's content that's used to detect whether any of the boot resources have changed. Blazor caches downloaded files using the browser [Cache](https://developer.mozilla.org/docs/Web/API/Cache) API. -When Blazor WebAssembly downloads an app's startup files, it instructs the browser to perform integrity checks on the responses. Blazor sends SHA-256 hash values for DLL (`.dll`), WebAssembly (`.wasm`), and other files in the `blazor.boot.json` file, which isn't cached on clients. The file hashes of cached files are compared to the hashes in the `blazor.boot.json` file. For cached files with a matching hash, Blazor uses the cached files. Otherwise, files are requested from the server. After a file is downloaded, its hash is checked again for integrity validation. An error is generated by the browser if any downloaded file's integrity check fails. +When Blazor WebAssembly downloads an app's startup files, it instructs the browser to perform integrity checks on the responses. Blazor sends SHA-256 hash values for DLL (`.dll`), WebAssembly (`.wasm`), and other files in the Blazor boot configuration, which isn't cached on clients. The file hashes of cached files are compared to the hashes in the Blazor boot configuration. For cached files with a matching hash, Blazor uses the cached files. Otherwise, files are requested from the server. After a file is downloaded, its hash is checked again for integrity validation. An error is generated by the browser if any downloaded file's integrity check fails. Blazor's algorithm for managing file integrity: @@ -42,7 +42,7 @@ For Blazor WebAssembly's boot reference source, see [the `Boot.WebAssembly.ts` f ## Diagnose integrity problems -When an app is built, the generated `blazor.boot.json` manifest describes the SHA-256 hashes of boot resources at the time that the build output is produced. The integrity check passes as long as the SHA-256 hashes in `blazor.boot.json` match the files delivered to the browser. +When an app is built, the generated boot manifest describes the SHA-256 hashes of boot resources at the time that the build output is produced. The integrity check passes as long as the SHA-256 hashes in the boot manifest match the files delivered to the browser. Common reasons why this fails include: @@ -51,23 +51,38 @@ Common reasons why this fails include: * If you or build tools manually modify the build output. * If some aspect of the deployment process modified the files. For example if you use a Git-based deployment mechanism, bear in mind that Git transparently converts Windows-style line endings to Unix-style line endings if you commit files on Windows and check them out on Linux. Changing file line endings change the SHA-256 hashes. To avoid this problem, consider [using `.gitattributes` to treat build artifacts as `binary` files](https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes). * The web server modifies the file contents as part of serving them. For example, some content distribution networks (CDNs) automatically attempt to [minify](xref:client-side/bundling-and-minification#minification) HTML, thereby modifying it. You may need to disable such features. -* The `blazor.boot.json` file fails to load properly or is improperly cached on the client. Common causes include either of the following: +* The boot manifest fails to load properly or is improperly cached on the client. Common causes include either of the following: * Misconfigured or malfunctioning custom developer code. * One or more misconfigured intermediate caching layers. To diagnose which of these applies in your case: - 1. Note which file is triggering the error by reading the error message. - 1. Open your browser's developer tools and look in the *Network* tab. If necessary, reload the page to see the list of requests and responses. Find the file that is triggering the error in that list. - 1. Check the HTTP status code in the response. If the server returns anything other than *200 - OK* (or another 2xx status code), then you have a server-side problem to diagnose. For example, status code 403 means there's an authorization problem, whereas status code 500 means the server is failing in an unspecified manner. Consult server-side logs to diagnose and fix the app. - 1. If the status code is *200 - OK* for the resource, look at the response content in browser's developer tools and check that the content matches up with the data expected. For example, a common problem is to misconfigure routing so that requests return your `index.html` data even for other files. Make sure that responses to `.wasm` requests are WebAssembly binaries and that responses to `.dll` requests are .NET assembly binaries. If not, you have a server-side routing problem to diagnose. - 1. Seek to validate the app's published and deployed output with the [Troubleshoot integrity PowerShell script](#troubleshoot-integrity-powershell-script). +:::moniker range=">= aspnetcore-10.0" + +1. Note which file is triggering the error by reading the error message. +1. Open your browser's developer tools and look in the *Network* tab. If necessary, reload the page to see the list of requests and responses. Find the file that is triggering the error in that list. +1. Check the HTTP status code in the response. If the server returns anything other than *200 - OK* (or another 2xx status code), then you have a server-side problem to diagnose. For example, status code 403 means there's an authorization problem, whereas status code 500 means the server is failing in an unspecified manner. Consult server-side logs to diagnose and fix the app. +1. If the status code is *200 - OK* for the resource, look at the response content in browser's developer tools and check that the content matches up with the data expected. For example, a common problem is to misconfigure routing so that requests return your `index.html` data even for other files. Make sure that responses to `.wasm` requests are WebAssembly binaries and that responses to `.dll` requests are .NET assembly binaries. If not, you have a server-side routing problem to diagnose. + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + +1. Note which file is triggering the error by reading the error message. +1. Open your browser's developer tools and look in the *Network* tab. If necessary, reload the page to see the list of requests and responses. Find the file that is triggering the error in that list. +1. Check the HTTP status code in the response. If the server returns anything other than *200 - OK* (or another 2xx status code), then you have a server-side problem to diagnose. For example, status code 403 means there's an authorization problem, whereas status code 500 means the server is failing in an unspecified manner. Consult server-side logs to diagnose and fix the app. +1. If the status code is *200 - OK* for the resource, look at the response content in browser's developer tools and check that the content matches up with the data expected. For example, a common problem is to misconfigure routing so that requests return your `index.html` data even for other files. Make sure that responses to `.wasm` requests are WebAssembly binaries and that responses to `.dll` requests are .NET assembly binaries. If not, you have a server-side routing problem to diagnose. +1. Seek to validate the app's published and deployed output with the [Troubleshoot integrity PowerShell script](#troubleshoot-integrity-powershell-script). + +:::moniker-end If you confirm that the server is returning plausibly correct data, there must be something else modifying the contents in between build and delivery of the file. To investigate this: * Examine the build toolchain and deployment mechanism in case they're modifying files after the files are built. An example of this is when Git transforms file line endings, as described earlier. * Examine the web server or CDN configuration in case they're set up to modify responses dynamically (for example, trying to minify HTML). It's fine for the web server to implement HTTP compression (for example, returning `content-encoding: br` or `content-encoding: gzip`), since this doesn't affect the result after decompression. However, it's *not* fine for the web server to modify the uncompressed data. +:::moniker range="< aspnetcore-10.0" + ## Troubleshoot integrity PowerShell script Use the [`integrity.ps1`](https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/blazor/host-and-deploy/webassembly/_samples/integrity.ps1?raw=true) PowerShell script to validate a published and deployed Blazor app. The script is provided for PowerShell Core 7 or later as a starting point when the app has integrity issues that the Blazor framework can't identify. Customization of the script might be required for your apps, including if running on version of PowerShell later than version 7.2.0. @@ -119,6 +134,8 @@ Placeholders: > > For more information, see [Overview of threat protection by Microsoft Defender Antivirus](/microsoft-365/business-premium/m365bp-threats-detected-defender-av). +:::moniker-end + ## Disable integrity checking for non-PWA apps In most cases, don't disable integrity checking. Disabling integrity checking doesn't solve the underlying problem that has caused the unexpected responses and results in losing the benefits listed earlier. diff --git a/aspnetcore/blazor/host-and-deploy/webassembly/github-pages.md b/aspnetcore/blazor/host-and-deploy/webassembly/github-pages.md index 1dc660d888c7..02b6e224daf6 100644 --- a/aspnetcore/blazor/host-and-deploy/webassembly/github-pages.md +++ b/aspnetcore/blazor/host-and-deploy/webassembly/github-pages.md @@ -52,10 +52,10 @@ For more information, see [Using pre-written building blocks in your workflow: U Configure the following entries in the script for your deployment: -* Publish directory (`PUBLISH_DIR`): Use the path to the repository's folder where the Blazor WebAssembly app is published. The app is compiled for a specific .NET version, and the path segment for the version must match. Example: `BlazorWebAssemblyXrefGenerator/bin/Release/net9.0/publish/wwwroot` is the path for an app that adopts the `net9.0` [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) for the .NET 9.0 SDK -* Push path (`on:push:paths`): Set the push path to match the app's repo folder with a `**` wildcard. Example: `BlazorWebAssemblyXrefGenerator/**` +* Publish directory (`PUBLISH_DIR`): Use the path to the repository's folder where the Blazor WebAssembly app is published. The app is compiled for a specific .NET version, and the path segment for the version must match. Example: `BlazorWebAssemblyXrefGenerator/bin/Release/net9.0/publish/wwwroot` is the path for an app that adopts the `net9.0` [Target Framework Moniker (TFM)](/dotnet/standard/frameworks) for the .NET 9.0 SDK. +* Push path (`on:push:paths`): Set the push path to match the app's repo folder with a `**` wildcard. Example: `BlazorWebAssemblyXrefGenerator/**`. * .NET SDK version (`dotnet-version` via the [`actions/setup-dotnet` Action](https://github.com/actions/setup-dotnet)): Currently, there's no way to set the version to "latest" (see [Allow specifying 'latest' as dotnet-version (`actions/setup-dotnet` #497)](https://github.com/actions/setup-dotnet/issues/497) to up-vote the feature request). Set the SDK version at least as high as the app's framework version. -* Publish path (`dotnet publish` command): Set the publish folder path to the app's repo folder. Example: `dotnet publish BlazorWebAssemblyXrefGenerator -c Release` +* Publish path (`dotnet publish` command): Set the publish folder path to the app's repo folder. Example: `dotnet publish BlazorWebAssemblyXrefGenerator -c Release`. * Base HREF (`base_href` for the [`SteveSandersonMS/ghaction-rewrite-base-href` Action](https://github.com/SteveSandersonMS/ghaction-rewrite-base-href)): Set the SHA hash for the latest version of the Action (see the guidance in the [*GitHub Pages settings*](#github-pages-settings) section for instructions). Set the base href for the app to the repository's name. Example: The Blazor sample's repository owner is `dotnet`. The Blazor sample's repository's name is `blazor-samples`. When the Xref Generator tool is deployed to GitHub Pages, its web address is based on the repository's name (`https://dotnet.github.io/blazor-samples/`). The base href of the app is `/blazor-samples/`, which is set into `base_href` for the `ghaction-rewrite-base-href` Action to write into the app's `wwwroot/index.html` `` tag when the app is deployed. For more information, see . The GitHub-hosted Ubuntu (latest) server has a version of the .NET SDK pre-installed. You can remove the [`actions/setup-dotnet` Action](https://github.com/actions/setup-dotnet) step from the `static.yml` script if the pre-installed .NET SDK is sufficient to compile the app. To determine the .NET SDK installed for `ubuntu-latest`: @@ -69,7 +69,7 @@ The GitHub-hosted Ubuntu (latest) server has a version of the .NET SDK pre-insta The default GitHub Action, which deploys pages, skips deployment of folders starting with underscore, the `_framework` folder for example. To deploy folders starting with underscore, add an empty `.nojekyll` file to the root of the app's repository. Example: [Xref Generator `.nojekyll` file](https://github.com/dotnet/blazor-samples/blob/main/BlazorWebAssemblyXrefGenerator/.nojekyll) -***Perform this step before the first app deployment:*** Git treats JavaScript (JS) files, such as `blazor.webassembly.js`, as text and converts line endings from CRLF (carriage return-line feed) to LF (line feed) in the deployment pipeline. These changes to JS files produce different file hashes than Blazor sends to the client in the `blazor.boot.json` file. The mismatches result in integrity check failures on the client. One approach to solving this problem is to add a `.gitattributes` file with `*.js binary` line before adding the app's assets to the Git branch. The `*.js binary` line configures Git to treat JS files as binary files, which avoids processing the files in the deployment pipeline. The file hashes of the unprocessed files match the entries in the `blazor.boot.json` file, and client-side integrity checks pass. For more information, see . Example: [Xref Generator `.gitattributes` file](https://github.com/dotnet/blazor-samples/blob/main/BlazorWebAssemblyXrefGenerator/.gitattributes) +***Perform this step before the first app deployment:*** Git treats JavaScript (JS) files, such as `blazor.webassembly.js`, as text and converts line endings from CRLF (carriage return-line feed) to LF (line feed) in the deployment pipeline. These changes to JS files produce different file hashes than Blazor sends to the client. The mismatches result in integrity check failures on the client. One approach to solving this problem is to add a `.gitattributes` file with `*.js binary` line before adding the app's assets to the Git branch. The `*.js binary` line configures Git to treat JS files as binary files, which avoids processing the files in the deployment pipeline and results in client-side integrity checks passing. For more information, see . Example: [Xref Generator `.gitattributes` file](https://github.com/dotnet/blazor-samples/blob/main/BlazorWebAssemblyXrefGenerator/.gitattributes) To handle URL rewrites based on [Single Page Apps for GitHub Pages (`rafrex/spa-github-pages` GitHub repository)](https://github.com/rafrex/spa-github-pages): diff --git a/aspnetcore/blazor/host-and-deploy/webassembly/index.md b/aspnetcore/blazor/host-and-deploy/webassembly/index.md index b9d8195115d3..ae9ea56ec273 100644 --- a/aspnetcore/blazor/host-and-deploy/webassembly/index.md +++ b/aspnetcore/blazor/host-and-deploy/webassembly/index.md @@ -482,21 +482,38 @@ In the following examples: * PowerShell (PS) is used to update the file extensions. * `.dll` files are renamed to use the `.bin` file extension from the command line. -* Files listed in the published `blazor.boot.json` file with a `.dll` file extension are updated to the `.bin` file extension. +* Files listed in the published Blazor boot manifest with a `.dll` file extension are updated to the `.bin` file extension. * If service worker assets are also in use, a PowerShell command updates the `.dll` files listed in the `service-worker-assets.js` file to the `.bin` file extension. To use a different file extension than `.bin`, replace `.bin` in the following commands with the desired file extension. On Windows: +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +```powershell +dir {PATH} | rename-item -NewName { $_.name -replace ".dll\b",".bin" } +((Get-Content {PATH}\dotnet.boot.js -Raw) -replace '.dll"','.bin"') | Set-Content {PATH}\dotnet.boot.js +``` + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + ```powershell dir {PATH} | rename-item -NewName { $_.name -replace ".dll\b",".bin" } ((Get-Content {PATH}\blazor.boot.json -Raw) -replace '.dll"','.bin"') | Set-Content {PATH}\blazor.boot.json ``` -In the preceding command, the `{PATH}` placeholder is the path to the published `_framework` folder (for example, `.\bin\Release\net6.0\browser-wasm\publish\wwwroot\_framework` from the project's root folder). +:::moniker-end + +:::moniker range=">= aspnetcore-5.0" + +In the preceding command, the `{PATH}` placeholder is the path to the published `_framework` folder (for example, `.\bin\Release\{TFM}\browser-wasm\publish\wwwroot\_framework` from the project's root folder, where the `{TFM}` placeholder is the [target framework moniker (TFM)](/dotnet/standard/frameworks)). -If service worker assets are also in use: +If service worker assets are also in use because the app is a [Progressive Web App (PWA)](xref:blazor/progressive-web-app): ```powershell ((Get-Content {PATH}\service-worker-assets.js -Raw) -replace '.dll"','.bin"') | Set-Content {PATH}\service-worker-assets.js @@ -506,14 +523,31 @@ In the preceding command, the `{PATH}` placeholder is the path to the published On Linux or macOS: +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +```console +for f in {PATH}/*; do mv "$f" "`echo $f | sed -e 's/\.dll/.bin/g'`"; done +sed -i 's/\.dll"/.bin"/g' {PATH}/dotnet.boot.js +``` + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + ```console for f in {PATH}/*; do mv "$f" "`echo $f | sed -e 's/\.dll/.bin/g'`"; done sed -i 's/\.dll"/.bin"/g' {PATH}/blazor.boot.json ``` -In the preceding command, the `{PATH}` placeholder is the path to the published `_framework` folder (for example, `.\bin\Release\net6.0\browser-wasm\publish\wwwroot\_framework` from the project's root folder). +:::moniker-end + +:::moniker range=">= aspnetcore-5.0" + +In the preceding command, the `{PATH}` placeholder is the path to the published `_framework` folder (for example, `.\bin\Release\{TFM}\browser-wasm\publish\wwwroot\_framework` from the project's root folder), where the `{TFM}` placeholder is the [target framework moniker (TFM)](/dotnet/standard/frameworks)) -If service worker assets are also in use: +If service worker assets are also in use because the app is a [Progressive Web App (PWA)](xref:blazor/progressive-web-app): ```console sed -i 's/\.dll"/.bin"/g' {PATH}/service-worker-assets.js @@ -521,6 +555,35 @@ sed -i 's/\.dll"/.bin"/g' {PATH}/service-worker-assets.js In the preceding command, the `{PATH}` placeholder is the path to the published `service-worker-assets.js` file. +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +To address the compressed `dotnet.boot.js.gz` and `dotnet.boot.js.br` files, adopt either of the following approaches: + +* Remove the compressed `dotnet.boot.js.gz` and `dotnet.boot.js.br` files. **Compression is disabled with this approach.** +* Recompress the updated `dotnet.boot.js` file. + +The preceding guidance for the compressed `dotnet.boot.js` file also applies when service worker assets are in use. Remove or recompress `service-worker-assets.js.br` and `service-worker-assets.js.gz`. Otherwise, file integrity checks fail in the browser. + +The following Windows example for .NET 6 or later uses a PowerShell script placed at the root of the project. The following script, which disables compression, is the basis for further modification if you wish to recompress the `dotnet.boot.js` file. Pass the app's path and TFM to the script. + +`ChangeDLLExtensions.ps1:`: + +```powershell +param([string]$filepath,[string]$tfm) +dir $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework | rename-item -NewName { $_.name -replace ".dll\b",".bin" } +((Get-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\dotnet.boot.js -Raw) -replace '.dll"','.bin"') | Set-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\dotnet.boot.js +Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\dotnet.boot.js.gz +Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\dotnet.boot.js.br +``` + +Recompress `dotnet.boot.js` to re-enable compression. + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + To address the compressed `blazor.boot.json.gz` and `blazor.boot.json.br` files, adopt either of the following approaches: * Remove the compressed `blazor.boot.json.gz` and `blazor.boot.json.br` files. **Compression is disabled with this approach.** @@ -528,7 +591,7 @@ To address the compressed `blazor.boot.json.gz` and `blazor.boot.json.br` files, The preceding guidance for the compressed `blazor.boot.json` file also applies when service worker assets are in use. Remove or recompress `service-worker-assets.js.br` and `service-worker-assets.js.gz`. Otherwise, file integrity checks fail in the browser. -The following Windows example for .NET 6 uses a PowerShell script placed at the root of the project. The following script, which disables compression, is the basis for further modification if you wish to recompress the `blazor.boot.json` file. +The following Windows example for .NET 6 to .NET 9 uses a PowerShell script placed at the root of the project. The following script, which disables compression, is the basis for further modification if you wish to recompress the `blazor.boot.json` file. Pass the app's path and TFM to the script. `ChangeDLLExtensions.ps1:`: @@ -540,14 +603,22 @@ Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\b Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\blazor.boot.json.br ``` -If service worker assets are also in use, add the following commands: +Recompress `blazor.boot.json` to re-enable compression. + +:::moniker-end + +:::moniker range=">= aspnetcore-5.0" + +If service worker assets are also in use because the app is a [Progressive Web App (PWA)](xref:blazor/progressive-web-app), add the following commands: ```powershell -((Get-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\service-worker-assets.js -Raw) -replace '.dll"','.bin"') | Set-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js -Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js.gz -Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\_framework\wwwroot\service-worker-assets.js.br +((Get-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\service-worker-assets.js -Raw) -replace '.dll"','.bin"') | Set-Content $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\service-worker-assets.js +Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\service-worker-assets.js.gz +Remove-Item $filepath\bin\Release\$tfm\browser-wasm\publish\wwwroot\service-worker-assets.js.br ``` +Recompress `service-worker-assets.js` to re-enable compression. + In the project file, the script is executed after publishing the app for the `Release` configuration: ```xml diff --git a/aspnetcore/blazor/progressive-web-app.md b/aspnetcore/blazor/progressive-web-app.md index b021416a53ef..6eacef7c0a89 100644 --- a/aspnetcore/blazor/progressive-web-app.md +++ b/aspnetcore/blazor/progressive-web-app.md @@ -405,5 +405,15 @@ The [`CarChecker`](https://github.com/SteveSandersonMS/CarChecker) sample app de ## Additional resources +:::moniker range=">= aspnetcore-10.0" + +[Client-side SignalR cross-origin negotiation for authentication](xref:blazor/fundamentals/signalr#client-side-signalr-cross-origin-negotiation-for-authentication) + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + * [Troubleshoot integrity PowerShell script](xref:blazor/host-and-deploy/webassembly/bundle-caching-and-integrity-check-failures#troubleshoot-integrity-powershell-script) * [Client-side SignalR cross-origin negotiation for authentication](xref:blazor/fundamentals/signalr#client-side-signalr-cross-origin-negotiation-for-authentication) + +:::moniker-end diff --git a/aspnetcore/blazor/webassembly-lazy-load-assemblies.md b/aspnetcore/blazor/webassembly-lazy-load-assemblies.md index 8d5c24bd9e35..8b9467bb622a 100644 --- a/aspnetcore/blazor/webassembly-lazy-load-assemblies.md +++ b/aspnetcore/blazor/webassembly-lazy-load-assemblies.md @@ -353,7 +353,7 @@ For more information, see callback and the assembly names in the `blazor.boot.json` file are out of sync. +The resource loader relies on the assembly names that are defined in the boot manifest file. If [assemblies are renamed](xref:blazor/host-and-deploy/webassembly/index#change-the-file-name-extension-of-dll-files), the assembly names used in an callback and the assembly names in the boot manifest file are out of sync. To rectify this: diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index 132a63bb47f3..7a0938948ed9 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -202,4 +202,11 @@ The default environments are: * `Development` for build. * `Production` for publish. +## Boot configuration file name change + +The boot configuration file is changing names from `blazor.boot.json` to `dotnet.boot.js`. This name change only affects developers who are interacting directly with the file, such as when developers are: + +* Checking file integrity for published assets with the troubleshoot integrity PowerShell script per the guidance in . +* Changing the file name extension of DLL files when not using the default Webcil file format per the guidance in . + --> From f5ea8eb0fe647b969c048249bd00d7e1020d9c49 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 10 Apr 2025 12:28:03 -0400 Subject: [PATCH 6/8] [Pre3] Declaratively persist state (#35198) --- .../integration-hosted-webassembly.md | 8 +- aspnetcore/blazor/components/integration.md | 93 ++++++- aspnetcore/blazor/components/lifecycle.md | 48 +++- aspnetcore/blazor/components/prerender.md | 246 +++++++++++++++++- aspnetcore/blazor/security/index.md | 65 +++++ .../aspnetcore-10/includes/blazor.md | 82 +++++- 6 files changed, 527 insertions(+), 15 deletions(-) diff --git a/aspnetcore/blazor/components/integration-hosted-webassembly.md b/aspnetcore/blazor/components/integration-hosted-webassembly.md index 6f80a7befae1..bb7a36bc0606 100644 --- a/aspnetcore/blazor/components/integration-hosted-webassembly.md +++ b/aspnetcore/blazor/components/integration-hosted-webassembly.md @@ -515,7 +515,7 @@ else protected override async Task OnInitializedAsync() { if (!ApplicationState.TryTakeFromJson( - "fetchdata", out var restored)) + nameof(forecasts), out var restored)) { forecasts = await WeatherForecastService.GetForecastAsync( DateOnly.FromDateTime(DateTime.Now)); @@ -531,7 +531,7 @@ else private Task PersistData() { - ApplicationState.PersistAsJson("fetchdata", forecasts); + ApplicationState.PersistAsJson(nameof(forecasts), forecasts); return Task.CompletedTask; } @@ -1030,7 +1030,7 @@ else protected override async Task OnInitializedAsync() { if (!ApplicationState.TryTakeFromJson( - "fetchdata", out var restored)) + nameof(forecasts), out var restored)) { forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now); @@ -1046,7 +1046,7 @@ else private Task PersistData() { - ApplicationState.PersistAsJson("fetchdata", forecasts); + ApplicationState.PersistAsJson(nameof(forecasts), forecasts); return Task.CompletedTask; } diff --git a/aspnetcore/blazor/components/integration.md b/aspnetcore/blazor/components/integration.md index b51fe0bb4adc..7c354d11915d 100644 --- a/aspnetcore/blazor/components/integration.md +++ b/aspnetcore/blazor/components/integration.md @@ -402,6 +402,83 @@ In `Pages/_Host.cshtml` of Blazor apps that are `ServerPrerendered` in a Blazor ``` +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + + + +Decide what state to persist using the service. The `[SupplyParameterFromPersistentComponentState]` attribute applied to a property registers a callback to persist the state during prerendering and loads it when the component renders interactively or the service is instantiated. + +In the following example, the `{TYPE}` placeholder represents the type of data to persist (for example, `WeatherForecast[]`). + +```razor +@code { + [SupplyParameterFromPersistentComponentState] + public {TYPE} Data { get; set; } +} +``` + +In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper) persists the component state after all component invocations. + +`WeatherForecastPreserveState.razor`: + +```razor +@page "/weather-forecast-preserve-state" +@using BlazorSample.Shared +@inject IWeatherForecastService WeatherForecastService + +Weather Forecast + +

Weather forecast

+ +

This component demonstrates fetching data from the server.

+ +@if (Forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in Forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + [SupplyParameterFromPersistentComponentState] + public WeatherForecast[]? Forecasts { get; set; } + + protected override async Task OnInitializedAsync() + { + Forecasts ??= await WeatherForecastService.GetForecastAsync( + DateOnly.FromDateTime(DateTime.Now)); + } +} +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-7.0 < aspnetcore-10.0" + Decide what state to persist using the service. registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. In the following example: @@ -449,7 +526,7 @@ In the following example: } ``` -The following example is an updated version of the `FetchData` component based on the Blazor project template. The `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper) persists the component state after all component invocations. +In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper) persists the component state after all component invocations. `Pages/WeatherForecastPreserveState.razor`: @@ -502,7 +579,7 @@ else protected override async Task OnInitializedAsync() { if (!ApplicationState.TryTakeFromJson( - "fetchdata", out var restored)) + nameof(forecasts), out var restored)) { forecasts = await WeatherForecastService.GetForecastAsync( @@ -519,7 +596,7 @@ else private Task PersistData() { - ApplicationState.PersistAsJson("fetchdata", forecasts); + ApplicationState.PersistAsJson(nameof(forecasts), forecasts); return Task.CompletedTask; } @@ -531,6 +608,10 @@ else } ``` +:::moniker-end + +:::moniker range=">= aspnetcore-7.0" + By initializing components with the same state used during prerendering, any expensive initialization steps are only executed once. The rendered UI also matches the prerendered UI, so no flicker occurs in the browser. The persisted prerendered state is transferred to the client, where it's used to restore the component state. [ASP.NET Core Data Protection](xref:security/data-protection/introduction) ensures that the data is transferred securely in Blazor Server apps. @@ -969,7 +1050,7 @@ To solve these problems, Blazor supports persisting state in a prerendered page Decide what state to persist using the service. registers a callback to persist the component state before the app is paused. The state is retrieved when the application resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. -The following example is an updated version of the `FetchData` component based on the Blazor project template. The `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper) persists the component state after all component invocations. +In the following example, the `WeatherForecastPreserveState` component persists weather forecast state during prerendering and then retrieves the state to initialize the component. The [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper) persists the component state after all component invocations. `Pages/WeatherForecastPreserveState.razor`: @@ -1022,7 +1103,7 @@ else protected override async Task OnInitializedAsync() { if (!ApplicationState.TryTakeFromJson( - "fetchdata", out var restored)) + nameof(forecasts), out var restored)) { forecasts = await WeatherForecastService.GetForecastAsync(DateTime.Now); @@ -1038,7 +1119,7 @@ else private Task PersistData() { - ApplicationState.PersistAsJson("fetchdata", forecasts); + ApplicationState.PersistAsJson(nameof(forecasts), forecasts); return Task.CompletedTask; } diff --git a/aspnetcore/blazor/components/lifecycle.md b/aspnetcore/blazor/components/lifecycle.md index 4a7f5ad4c9af..235ec3b4b178 100644 --- a/aspnetcore/blazor/components/lifecycle.md +++ b/aspnetcore/blazor/components/lifecycle.md @@ -594,7 +594,7 @@ Prerendering waits for *quiescence*, which means that a component doesn't render Welcome to your new app. - + ``` > [!NOTE] @@ -616,6 +616,46 @@ When the `Home` component is prerendering, the `Slow` component is quickly rende To address the double rendering of the loading message and the re-execution of service and database calls, persist prerendered state with for final rendering of the component, as seen in the following updates to the `Slow` component: +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +```razor +@page "/slow" +@attribute [StreamRendering] + +

Slow Component

+ +@if (Data is null) +{ +
Loading...
+} +else +{ +
@Data
+} + +@code { + [SupplyParameterFromPersistentComponentState] + public string? Data { get; set; } + + protected override async Task OnInitializedAsync() + { + Data ??= await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + await Task.Delay(10000); + return "Finished!"; + } +} +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0" + ```razor @page "/slow" @attribute [StreamRendering] @@ -639,7 +679,7 @@ else protected override async Task OnInitializedAsync() { - if (!ApplicationState.TryTakeFromJson("data", out var restored)) + if (!ApplicationState.TryTakeFromJson(nameof(data), out var restored)) { data = await LoadDataAsync(); } @@ -654,7 +694,7 @@ else private Task PersistData() { - ApplicationState.PersistAsJson("data", data); + ApplicationState.PersistAsJson(nameof(data), data); return Task.CompletedTask; } @@ -672,6 +712,8 @@ else } ``` +:::moniker-end + By combining streaming rendering with persistent component state: * Services and databases only require a single call for component initialization. diff --git a/aspnetcore/blazor/components/prerender.md b/aspnetcore/blazor/components/prerender.md index 1fdbe8b965c5..c14cbc9cc821 100644 --- a/aspnetcore/blazor/components/prerender.md +++ b/aspnetcore/blazor/components/prerender.md @@ -45,7 +45,249 @@ The first logged count occurs during prerendering. The count is set again after To retain the initial value of the counter during prerendering, Blazor supports persisting state in a prerendered page using the service (and for components embedded into pages or views of Razor Pages or MVC apps, the [Persist Component State Tag Helper](xref:mvc/views/tag-helpers/builtin-th/persist-component-state-tag-helper)). -To preserve prerendered state, decide what state to persist using the service. registers a callback to persist the component state before the app is paused. The state is retrieved when the app resumes. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. +:::moniker range=">= aspnetcore-10.0" + + + +To preserve prerendered state, use the `[SupplyParameterFromPersistentComponentState]` attribute to persist state in properties. Properties with this attribute are automatically persisted using the service during prerendering. The state is retrieved when the component renders interactively or the service is instantiated. + +By default, properties are serialized using the serializer with default settings. Serialization isn't trimmer safe and requires preservation of the types used. For more information, see . + +The following example demonstrates the general pattern, where the `{TYPE}` placeholder represents the type of data to persist. + +```razor +@code { + [SupplyParameterFromPersistentComponentState] + public {TYPE} Data { get; set; } + + protected override async Task OnInitializedAsync() + { + Data ??= await ...; + } +} +``` + +The following counter component example persists counter state during prerendering and retrieves the state to initialize the component. + +`PrerenderedCounter2.razor`: + +```razor +@page "/prerendered-counter-2" +@inject ILogger Logger + +Prerendered Counter 2 + +

Prerendered Counter 2

+ +

Current count: @CurrentCount

+ + + +@code { + [SupplyParameterFromPersistentComponentState] + public int CurrentCount { get; set; } + + protected override void OnInitialized() + { + CurrentCount ??= Random.Shared.Next(100); + Logger.LogInformation("CurrentCount set to {Count}", CurrentCount); + } + + private void IncrementCount() => CurrentCount++; +} +``` + +In the following example that serializes state for multiple components of the same type: + +* Properties annotated with the `[SupplyParameterFromPersistentComponentState]` attribute are serialized and deserialized during prerendering. +* The [`@key` directive attribute](xref:blazor/components/key#use-of-the-key-directive-attribute) is used to ensure that the state is correctly associated with the component instance. +* The `Element` property is initialized in the [`OnInitialized` lifecycle method](xref:blazor/components/lifecycle#component-initialization-oninitializedasync) to avoid null reference exceptions, similarly to how null references are avoided for query parameters and form data. + +`PersistentChild.razor`: + +```razor +
+

Current count: @Element.CurrentCount

+ +
+ +@code { + [SupplyParameterFromPersistentComponentState] + public State Element { get; set; } + + protected override void OnInitialized() + { + Element ??= new State(); + } + + private void IncrementCount() + { + Element.CurrentCount++; + } + + private class State + { + public int CurrentCount { get; set; } + } +} +``` + +`Parent.razor`: + +```razor +@page "/parent" + +@foreach (var element in elements) +{ + +} +``` + +In the following example that serializes state for a service: + +* Properties annotated with the `[SupplyParameterFromPersistentComponentState]` attribute are serialized during prerendering and deserialized when the app becomes interactive. +* The `AddPersistentService` method is used to register the service for persistence. The render mode is required because the render mode can't be inferred from the service type. Use any of the following values: + * `RenderMode.Server`: The service is available for the Interactive Server render mode. + * `RenderMode.Webassembly`: The service is available for the Interactive Webassembly render mode. + * `RenderMode.InteractiveAuto`: The service is available for both the Interactive Server and Interactive Webassembly render modes if a component renders in either of those modes. +* The service is resolved during the initialization of an interactive render mode, and the properties annotated with the `[SupplyParameterFromPersistentComponentState]` attribute are deserialized. + +> [!NOTE] +> Only persisting scoped services is supported. + +`CounterService.cs`: + +```csharp +public class CounterService +{ + [SupplyParameterFromPersistentComponentState] + public int CurrentCount { get; set; } + + public void IncrementCount() + { + CurrentCount++; + } +} +``` + +In `Program.cs`: + +```csharp +builder.Services.AddPersistentService(RenderMode.InteractiveAuto); +``` + +Serialized properties are identified from the actual service instance: + +* This approach allows marking an abstraction as a persistent service. +* Enables actual implementations to be internal or different types. +* Supports shared code in different assemblies. +* Results in each instance exposing the same properties. + +As an alternative to using the declarative model for persisting state with the `[SupplyParameterFromPersistentComponentState]` attribute, you can use the service directly, which offers greater flexibility for complex state persistence scenarios. Call to register a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. + +The following example demonstrates the general pattern: + +* The `{TYPE}` placeholder represents the type of data to persist. +* The `{TOKEN}` placeholder is a state identifier string. Consider using `nameof({VARIABLE})`, where the `{VARIABLE}` placeholder is the name of the variable that holds the state. Using [`nameof()`](/dotnet/csharp/language-reference/operators/nameof) for the state identifier avoids the use of a quoted string. + +```razor +@implements IDisposable +@inject PersistentComponentState ApplicationState + +... + +@code { + private {TYPE} data; + private PersistingComponentStateSubscription persistingSubscription; + + protected override async Task OnInitializedAsync() + { + if (!ApplicationState.TryTakeFromJson<{TYPE}>( + "{TOKEN}", out var restored)) + { + data = await ...; + } + else + { + data = restored!; + } + + // Call at the end to avoid a potential race condition at app shutdown + persistingSubscription = ApplicationState.RegisterOnPersisting(PersistData); + } + + private Task PersistData() + { + ApplicationState.PersistAsJson("{TOKEN}", data); + + return Task.CompletedTask; + } + + void IDisposable.Dispose() + { + persistingSubscription.Dispose(); + } +} +``` + +The following counter component example persists counter state during prerendering and retrieves the state to initialize the component. + +`PrerenderedCounter3.razor`: + +```razor +@page "/prerendered-counter-3" +@implements IDisposable +@inject ILogger Logger +@inject PersistentComponentState ApplicationState + +Prerendered Counter 3 + +

Prerendered Counter 3

+ +

Current count: @currentCount

+ + + +@code { + private int currentCount; + private PersistingComponentStateSubscription persistingSubscription; + + protected override void OnInitialized() + { + if (!ApplicationState.TryTakeFromJson( + nameof(currentCount), out var restoredCount)) + { + currentCount = Random.Shared.Next(100); + Logger.LogInformation("currentCount set to {Count}", currentCount); + } + else + { + currentCount = restoredCount!; + Logger.LogInformation("currentCount restored to {Count}", currentCount); + } + + // Call at the end to avoid a potential race condition at app shutdown + persistingSubscription = ApplicationState.RegisterOnPersisting(PersistCount); + } + + private Task PersistCount() + { + ApplicationState.PersistAsJson(nameof(currentCount), currentCount); + + return Task.CompletedTask; + } + + private void IncrementCount() => currentCount++; + + void IDisposable.Dispose() => persistingSubscription.Dispose(); +} +``` + +:::moniker-end + +:::moniker range="< aspnetcore-10.0" + +To preserve prerendered state, decide what state to persist using the service. registers a callback to persist the component state during prerendering. The state is retrieved when the component renders interactively. Make the call at the end of initialization code in order to avoid a potential race condition during app shutdown. The following example demonstrates the general pattern: @@ -98,6 +340,8 @@ The following counter component example persists counter state during prerenderi :::code language="razor" source="~/../blazor-samples/8.0/BlazorSample_BlazorWebApp/Components/Pages/PrerenderedCounter2.razor"::: +:::moniker-end + When the component executes, `currentCount` is only set once during prerendering. The value is restored when the component is rerendered. The following is example output. > [!NOTE] diff --git a/aspnetcore/blazor/security/index.md b/aspnetcore/blazor/security/index.md index 9764666239a7..e7db86283e7e 100644 --- a/aspnetcore/blazor/security/index.md +++ b/aspnetcore/blazor/security/index.md @@ -629,6 +629,67 @@ The client project maintains a `Weather` component that: * Enforces authorization with an [`[Authorize]` attribute](xref:Microsoft.AspNetCore.Authorization.AuthorizeAttribute). * Uses the [Persistent Component State service](xref:blazor/components/prerender#persist-prerendered-state) () to persist weather forecast data when the component transitions from static to interactive SSR on the server. For more information, see . +:::moniker-end + +:::moniker range=">= aspnetcore-10.0" + +```razor +@page "/weather" +@using Microsoft.AspNetCore.Authorization +@using BlazorWebAppEntra.Client.Weather +@attribute [Authorize] +@inject IWeatherForecaster WeatherForecaster + +Weather + +

Weather

+ +

This component demonstrates showing data.

+ +@if (Forecasts == null) +{ +

Loading...

+} +else +{ + + + + + + + + + + + @foreach (var forecast in Forecasts) + { + + + + + + + } + +
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
+} + +@code { + [SupplyParameterFromPersistentComponentState] + public IEnumerable? Forecasts { get; set; } + + protected override async Task OnInitializedAsync() + { + Forecasts ??= await WeatherForecaster.GetWeatherForecastAsync(); + } +} +``` + +:::moniker-end + +:::moniker range=">= aspnetcore-8.0 < aspnetcore-10.0" + ```razor @page "/weather" @using Microsoft.AspNetCore.Authorization @@ -704,6 +765,10 @@ else } ``` +:::moniker-end + +:::moniker range=">= aspnetcore-8.0" + The server project implements `IWeatherForecaster` as `ServerWeatherForecaster`, which generates and returns mock weather data via its `GetWeatherForecastAsync` method: ```csharp diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index 7a0938948ed9..a81dddc356b9 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -202,11 +202,91 @@ The default environments are: * `Development` for build. * `Production` for publish. -## Boot configuration file name change +### Boot configuration file name change The boot configuration file is changing names from `blazor.boot.json` to `dotnet.boot.js`. This name change only affects developers who are interacting directly with the file, such as when developers are: * Checking file integrity for published assets with the troubleshoot integrity PowerShell script per the guidance in . * Changing the file name extension of DLL files when not using the default Webcil file format per the guidance in . +### Declarative model for persisting state from components and services + +You can now declaratively specify state to persist from components and services using the `[SupplyParameterFromPersistentComponentState]` attribute. Properties with this attribute are automatically persisted using the service during prerendering. The state is retrieved when the component renders interactively or the service is instantiated. + +In previous Blazor releases, persisting component state during prerendering using the service involved a significant amount of code, as the following example demonstrates: + +```razor +@page "/movies" +@implements IDisposable +@inject IMovieService MovieService +@inject PersistentComponentState ApplicationState + +@if (MoviesList == null) +{ +

Loading...

+} +else +{ + + ... + +} + +@code { + public List? MoviesList { get; set; } + private PersistingComponentStateSubscription? persistingSubscription; + + protected override async Task OnInitializedAsync() + { + if (!ApplicationState.TryTakeFromJson>(nameof(MoviesList), + out var movies)) + { + MoviesList = await MovieService.GetMoviesAsync(); + } + else + { + MoviesList = movies; + } + + persistingSubscription = ApplicationState.RegisterOnPersisting(() => + { + ApplicationState.PersistAsJson(nameof(MoviesList), MoviesList); + return Task.CompletedTask; + }); + } + + public void Dispose() => persistingSubscription?.Dispose(); +} +``` + +This code can now be simplified using the new declarative model: + +```razor +@page "/movies" +@inject IMovieService MovieService + +@if (MoviesList == null) +{ +

Loading...

+} +else +{ + + ... + +} + +@code { + [SupplyParameterFromPersistentComponentState] + public List? MoviesList { get; set; } + + protected override async Task OnInitializedAsync() + { + MoviesList ??= await MovieService.GetMoviesAsync(); + } +} +``` + +For more information, see . Additional API implementation notes, which are subject to change at any time, are available in [[Blazor] Support for declaratively persisting component and services state (`dotnet/aspnetcore` #60634)](https://github.com/dotnet/aspnetcore/pull/60634). + --> From c897b43e2616d0bdfe29918ad67578a415077a71 Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:36:32 -0400 Subject: [PATCH 7/8] Blazor Pre3 final updates for release (#35200) --- aspnetcore/blazor/call-web-api.md | 2 ++ aspnetcore/blazor/fundamentals/routing.md | 8 +------ .../aspnetcore-10/includes/blazor.md | 24 ++++++------------- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/aspnetcore/blazor/call-web-api.md b/aspnetcore/blazor/call-web-api.md index be0aeb0d64bd..f55a9d32d100 100644 --- a/aspnetcore/blazor/call-web-api.md +++ b/aspnetcore/blazor/call-web-api.md @@ -992,6 +992,8 @@ To opt-out of response streaming globally, use either of the following approache * Set the `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` environment variable to `false` or `0`. +............. AND REMOVE THE NEXT LINE ............. + --> To opt-out of response streaming globally, set the `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` environment variable to `false` or `0`. diff --git a/aspnetcore/blazor/fundamentals/routing.md b/aspnetcore/blazor/fundamentals/routing.md index 5e01ac44267f..3ef9b12d7e3e 100644 --- a/aspnetcore/blazor/fundamentals/routing.md +++ b/aspnetcore/blazor/fundamentals/routing.md @@ -1626,13 +1626,7 @@ Use a component in place There are two options that you can assign to the `Match` attribute of the `` element: - - -* : The is active when it matches the current URL, ignoring the query string and fragment. To include matching on the query string/fragment, use the `Microsoft.AspNetCore.Components.Routing.NavLink.DisableMatchAllIgnoresLeftUriPart` [`AppContext` switch](/dotnet/fundamentals/runtime-libraries/system-appcontext). +* : The is active when it matches the current URL, ignoring the query string and fragment. To include matching on the query string/fragment, use the `Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragmentSwitchKey` [`AppContext` switch](/dotnet/fundamentals/runtime-libraries/system-appcontext) set to `true`. * (*default*): The is active when it matches any prefix of the current URL. :::moniker-end diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index a81dddc356b9..876f3df46f2f 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -47,7 +47,7 @@ For more information, see ` BELOW!!!!! - ### Response streaming is opt-in and how to opt-out In prior Blazor releases, response streaming for requests was opt-in. Now, response streaming is enabled by default. This is a breaking change because calling for an (`response.Content.ReadAsStreamAsync()`) returns a `BrowserHttpReadStream` and no longer a . `BrowserHttpReadStream` doesn't support synchronous operations, such as `Stream.Read(Span)`. If your code uses synchronous operations, you can opt-out of response streaming or copy the into a yourself. -DON'T USE (comment out) .............. + To opt-out of response streaming globally, set the `DOTNET_WASM_ENABLE_STREAMING_RESPONSE` environment variable to `false` or `0`. @@ -122,16 +124,6 @@ requestMessage.SetBrowserResponseStreamingEnabled(false); For more information, see [`HttpClient` and `HttpRequestMessage` with Fetch API request options (*Call web API* article)](xref:blazor/call-web-api?view=aspnetcore-10.0#httpclient-and-httprequestmessage-with-fetch-api-request-options). -XXXXXXXXXXXXXXXXXXXX CHANGE EARLIER COVERAGE XXXXXXXXXXXXXXXXXXXX - -In the "Ignore the query string and fragment when using `NavLinkMatch.All`" section, change -`DisableMatchAllIgnoresLeftUriPart` to `EnableMatchAllForQueryStringAndFragmentSwitchKey` -set to `true`. - -Also make this change in the *Routing* article at Line 1633. - -XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - ### Client-side fingerprinting Last year, the release of .NET 9 introduced [server-side fingerprinting](https://en.wikipedia.org/wiki/Fingerprint_(computing)) of static assets in Blazor Web Apps with the introduction of [Map Static Assets routing endpoint conventions (`MapStaticAssets`)](xref:fundamentals/map-static-files), the [`ImportMap` component](xref:blazor/fundamentals/static-files#importmap-component), and the property (`@Assets["..."]`) to resolve fingerprinted JavaScript modules. For .NET 10, you can opt-into client-side fingerprinting of JavaScript modules for standalone Blazor WebAssembly apps. @@ -288,5 +280,3 @@ else ``` For more information, see . Additional API implementation notes, which are subject to change at any time, are available in [[Blazor] Support for declaratively persisting component and services state (`dotnet/aspnetcore` #60634)](https://github.com/dotnet/aspnetcore/pull/60634). - ---> From f057de6444332eadd77920488eaa12674727ad1d Mon Sep 17 00:00:00 2001 From: Luke Latham <1622880+guardrex@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:20:05 -0400 Subject: [PATCH 8/8] Patch Blazor Pre3 API (#35201) --- aspnetcore/blazor/fundamentals/routing.md | 2 +- aspnetcore/release-notes/aspnetcore-10/includes/blazor.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aspnetcore/blazor/fundamentals/routing.md b/aspnetcore/blazor/fundamentals/routing.md index 3ef9b12d7e3e..5539d4a2baba 100644 --- a/aspnetcore/blazor/fundamentals/routing.md +++ b/aspnetcore/blazor/fundamentals/routing.md @@ -1626,7 +1626,7 @@ Use a component in place There are two options that you can assign to the `Match` attribute of the `` element: -* : The is active when it matches the current URL, ignoring the query string and fragment. To include matching on the query string/fragment, use the `Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragmentSwitchKey` [`AppContext` switch](/dotnet/fundamentals/runtime-libraries/system-appcontext) set to `true`. +* : The is active when it matches the current URL, ignoring the query string and fragment. To include matching on the query string/fragment, use the `Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment` [`AppContext` switch](/dotnet/fundamentals/runtime-libraries/system-appcontext) set to `true`. * (*default*): The is active when it matches any prefix of the current URL. :::moniker-end diff --git a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md index 876f3df46f2f..1d1cdce507c1 100644 --- a/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md +++ b/aspnetcore/release-notes/aspnetcore-10/includes/blazor.md @@ -47,7 +47,7 @@ For more information, see