Edit

Share via


What's new in ASP.NET Core 10.0

This article highlights the most significant changes in ASP.NET Core 10.0 with links to relevant documentation.

This article will be updated as new preview releases are made available. See the Asp.Net Core announcement page until this page is updated.

Blazor

This section describes new features for Blazor.

QuickGrid RowClass parameter

Apply a stylesheet class to a row of the grid based on the row item using the new RowClass parameter. In the following example, the GetRowCssClass method is called on each row to conditionally apply a stylesheet class based on the row item:

<QuickGrid ... RowClass="GetRowCssClass">
    ...
</QuickGrid>

@code {
    private string GetRowCssClass(MyGridItem item) =>
        item.IsArchived ? "row-archived" : null;
}

For more information, see ASP.NET Core Blazor `QuickGrid` component.

Blazor script as static web asset

In prior releases of .NET, the Blazor script is served from an embedded resource in the ASP.NET Core shared framework. In .NET 10 or later, the Blazor script is served as a static web asset with automatic compression and fingerprinting.

For more information, see the following resources:

Route template highlights

The [Route] attribute now supports route syntax highlighting to help visualize the structure of the route template:

Route template pattern of a route attribute for the counter value shows syntax highlighting

Previously, NavigationManager.NavigateTo scrolled to the top of the page for same-page navigations. This behavior has been changed in .NET 10 so that the browser no longer scrolls to the top of the page when navigating to the same page. This means the viewport is no longer reset when making updates to the address for the current page, such as changing the query string or fragment.

Reconnection UI component added to the Blazor Web App project template

The Blazor Web App project template now includes a ReconnectModal component, including collocated stylesheet and JavaScript files, for improved developer control over the reconnection UI when the client loses the WebSocket connection to the server. The component doesn't insert styles programmatically, ensuring compliance with stricter Content Security Policy (CSP) settings for the style-src policy. In prior releases, the default reconnection UI was created by the framework in a way that could cause CSP violations. Note that the default reconnection UI is still used as fallback when the app doesn't define the reconnection UI, such as by using the project template's ReconnectModal component or a similar custom component.

New reconnection UI features:

  • Apart from indicating the reconnection state by setting a specific CSS class on the reconnection UI element, the new components-reconnect-state-changed event is dispatched for reconnection state changes.
  • Code can better differentiate the stages of the reconnection process with the new reconnection state "retrying," indicated by both the CSS class and the new event.

For more information, see ASP.NET Core Blazor SignalR guidance.

Ignore the query string and fragment when using NavLinkMatch.All

The NavLink component now ignores the query string and fragment when using the NavLinkMatch.All value for the Match parameter. This means that the link retains the active class if the URL path matches but the query string or fragment change. To revert to the original behavior, use the Microsoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragment AppContext switch set to true.

You can also override the ShouldMatch method on NavLink to customize the matching behavior:

public class CustomNavLink : NavLink
{
    protected override bool ShouldMatch(string currentUriAbsolute)
    {
        // Custom matching logic
    }
}

For more information, see ASP.NET Core Blazor routing and navigation.

Close QuickGrid column options

You can now close the QuickGrid column options UI using the new CloseColumnOptionsAsync method.

The following example uses the CloseColumnOptionsAsync method to close the column options UI as soon as the title filter is applied:

<QuickGrid @ref="movieGrid" Items="movies">
    <PropertyColumn Property="@(m => m.Title)" Title="Title">
        <ColumnOptions>
            <input type="search" @bind="titleFilter" placeholder="Filter by title" 
                @bind:after="@(() => movieGrid.CloseColumnOptionsAsync())" />
        </ColumnOptions>
    </PropertyColumn>
    <PropertyColumn Property="@(m => m.Genre)" Title="Genre" />
    <PropertyColumn Property="@(m => m.ReleaseYear)" Title="Release Year" />
</QuickGrid>

@code {
    private QuickGrid<Movie>? movieGrid;
    private string titleFilter = string.Empty;
    private IQueryable<Movie> movies = new List<Movie> { ... }.AsQueryable();
    private IQueryable<Movie> filteredMovies => 
        movies.Where(m => m.Title!.Contains(titleFilter));
}

Response streaming is opt-in and how to opt-out

In prior Blazor releases, response streaming for HttpClient requests was opt-in. Now, response streaming is enabled by default.

This is a breaking change because calling HttpContent.ReadAsStreamAsync for an HttpResponseMessage.Content (response.Content.ReadAsStreamAsync()) returns a BrowserHttpReadStream and no longer a MemoryStream. BrowserHttpReadStream doesn't support synchronous operations, such as Stream.Read(Span<Byte>). If your code uses synchronous operations, you can opt-out of response streaming or copy the Stream into a MemoryStream yourself.

To opt-out of response streaming globally, set the DOTNET_WASM_ENABLE_STREAMING_RESPONSE environment variable to false or 0.

To opt-out of response streaming for an individual request, set SetBrowserResponseStreamingEnabled to false on the HttpRequestMessage (requestMessage in the following example):

requestMessage.SetBrowserResponseStreamingEnabled(false);

For more information, see HttpClient and HttpRequestMessage with Fetch API request options (Call web API article).

Client-side fingerprinting

Last year, the release of .NET 9 introduced server-side fingerprinting of static assets in Blazor Web Apps with the introduction of Map Static Assets routing endpoint conventions (MapStaticAssets), the ImportMap component, and the ComponentBase.Assets 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.

In standalone Blazor WebAssembly apps during build/publish, the framework overrides placeholders in index.html with values computed during build to fingerprint static assets. A fingerprint is placed into the blazor.webassembly.js script file name.

The following changes must be made in the wwwwoot/index.html file to adopt the fingerprinting feature. The standalone Blazor WebAssembly project template will be updated to include these changes in an upcoming preview release:

<head>
    ...
+   <script type="importmap"></script>
</head>

<body>
    ...
-   <script src="_framework/blazor.webassembly.js"></script>
+   <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>

</html>

In the project file (.csproj), add the <WriteImportMapToHtml> property set to true:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
+   <WriteImportMapToHtml>true</WriteImportMapToHtml>
  </PropertyGroup>
</Project>

To fingerprint additional JS modules in standalone Blazor WebAssembly apps, use the <StaticWebAssetFingerprintPattern> property in the app's project file (.csproj).

In the following example, a fingerprint is added for all developer-supplied .mjs files in the app:

<StaticWebAssetFingerprintPattern Include="JSModule" Pattern="*.mjs" 
  Expression="#[.{fingerprint}]!" />

The files are automatically placed into the import map:

  • Automatically for Blazor Web App CSR.
  • When opting-into module fingerprinting in standalone Blazor WebAssembly apps per the preceding instructions.

When resolving the import for JavaScript interop, the import map is used by the browser resolve fingerprinted files.

Set the environment in standalone Blazor WebAssembly apps

The Properties/launchSettings.json file is no longer used to control the environment in standalone Blazor WebAssembly apps.

Starting in .NET 10, set the environment with the <WasmApplicationEnvironmentName> property in the app's project file (.csproj).

The following example sets the app's environment to Staging:

<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>

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:

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 PersistentComponentState 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 PersistentComponentState service involved a significant amount of code, as the following example demonstrates:

@page "/movies"
@implements IDisposable
@inject IMovieService MovieService
@inject PersistentComponentState ApplicationState

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    public List<Movie>? MoviesList { get; set; }
    private PersistingComponentStateSubscription? persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        if (!ApplicationState.TryTakeFromJson<List<Movie>>(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:

@page "/movies"
@inject IMovieService MovieService

@if (MoviesList == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <QuickGrid Items="MoviesList.AsQueryable()">
        ...
    </QuickGrid>
}

@code {
    [SupplyParameterFromPersistentComponentState]
    public List<Movie>? MoviesList { get; set; }

    protected override async Task OnInitializedAsync()
    {
        MoviesList ??= await MovieService.GetMoviesAsync();
    }
}

For more information, see Prerender ASP.NET Core Razor components. 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).

Blazor Hybrid

This section describes new features for Blazor Hybrid.

New .NET MAUI Blazor Hybrid with a Blazor Web App and ASP.NET Core Identity article and sample

A new article and sample app has been added for .NET MAUI Blazor Hybrid and Web App using ASP.NET Core Identity.

For more information, see the following resources:

SignalR

This section describes new features for SignalR.

Minimal APIs

This section describes new features for minimal APIs.

Treating empty string in form post as null for nullable value types

When using the [FromForm] attribute with a complex object in minimal APIs, empty string values in a form post are now converted to null rather than causing a parse failure. This behavior matches the processing logic for form posts not associated with complex objects in minimal APIs.

using Microsoft.AspNetCore.Http;

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/todo", ([FromForm] Todo todo) => TypedResults.Ok(todo));

app.Run();

public class Todo
{
  public int Id { get; set; }
  public DateOnly? DueDate { get; set; } // Empty strings map to `null`
  public string Title { get; set; }
  public bool IsCompleted { get; set; }
}

Thanks to @nvmkpk for contributing this change!

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:

  • Query
  • Header
  • Request body

Validations are defined using attributes in the DataAnnotations namespace. Developers customize the behavior of the validation system by:

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:

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. 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, as in the following example:

app.MapPost("/products",
    ([EvenNumber(ErrorMessage = "Product ID must be even")] int productId, [Required] string name)
        => TypedResults.Ok(productId))
    .DisableValidation();

Support for Server-Sent Events (SSE)

ASP.NET Core now supports returning a ServerSentEvents result using the TypedResults.ServerSentEvents 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<T> objects, which may contain an event type, an ID, and a data payload of type T.

The TypedResults class has a new static method called ServerSentEvents that can be used to return a ServerSentEvents result. The first parameter to this method is an IAsyncEnumerable<SseItem<T>> 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:

app.MapGet("/json-item", (CancellationToken cancellationToken) =>
{
    async IAsyncEnumerable<HeartRateRecord> GetHeartRate(
        [EnumeratorCancellation] CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            var heartRate = Random.Shared.Next(60, 100);
            yield return HeartRateRecord.Create(heartRate);
            await Task.Delay(2000, cancellationToken);
        }
    }

    return TypedResults.ServerSentEvents(GetHeartRate(cancellationToken),
                                                  eventType: "heartRate");
});

For more information, see:

  • Server-Sent Events on MDN.
  • Minimal API sample app 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 using the TypedResults.ServerSentEvents API to return a stream of heart rate events as string, ServerSentEvents, and JSON objects to the client.

OpenAPI

This section describes new features for OpenAPI.

OpenAPI 3.1 support

ASP.NET Core has added support for generating OpenAPI version 3.1 documents in .NET 10. Despite the minor version bump, OpenAPI 3.1 is a significant update to the OpenAPI specification, in particular with full support for JSON Schema draft 2020-12.

Some of the changes you will see in the generated OpenAPI document include:

  • Nullable types no longer have the nullable: true property in the schema.
  • Instead of a nullable: true property, they have a type keyword whose value is an array that includes null as one of the types.
  • Properties or parameters defined as a C# int or long now appear in the generated OpenAPI document without the type: integer field and have a pattern field limiting the value to digits. This happens when the NumberHandling property in the JsonSerializerOptions is set to AllowReadingFromString, the default for ASP.NET Core Web apps. To enable C# int and long to be represented in the OpenAPI document as type: integer, set the NumberHandling property to Strict.

With this feature, the default OpenAPI version for generated documents is3.1. The version can be changed by explicitly setting the OpenApiVersion property of the OpenApiOptions in the configureOptions delegate parameter of AddOpenApi.

builder.Services.AddOpenApi(options =>
{
    // Specify the OpenAPI version to use.
    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});

When generating the OpenAPI document at build time, the OpenAPI version can be selected by setting the --openapi-version in the OpenApiGenerateDocumentsOptions MSBuild item.

    <!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.0 document. -->
    <OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_0</OpenApiGenerateDocumentsOptions>

OpenAPI 3.1 support was primarily added in the following PR.

OpenAPI 3.1 breaking changes

Support for OpenAPI 3.1 requires an update to the underlying OpenAPI.NET library to a new major version, 2.0. This new version has some breaking changes from the previous version. The breaking changes may impact apps if they have any document, operation, or schema transformers. Breaking changes in this iteration include the following:

  • Entities within the OpenAPI document, like operations and parameters, are typed as interfaces. Concrete implementations exist for the inlined and referenced variants of an entity. For example, an IOpenApiSchema can either be an inlined OpenApiSchema or an OpenApiSchemaReference that points to a schema defined elsewhere in the document.
  • The Nullable property has been removed from the OpenApiSchema type. To determine if a type is nullable, evaluate if the OpenApiSchema.Type property sets JsonSchemaType.Null.

One of the most significant changes is that the OpenApiAny class has been dropped in favor of using JsonNode directly. Transformers that use OpenApiAny need to be updated to use JsonNode. The following diff shows the changes in schema transformer from .NET 9 to .NET 10:

options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
    if (context.JsonTypeInfo.Type == typeof(WeatherForecast))
    {
-       schema.Example = new OpenApiObject
+       schema.Example = new JsonObject
        {
-           ["date"] = new OpenApiString(DateTime.Now.AddDays(1).ToString("yyyy-MM-dd")),
+           ["date"] = DateTime.Now.AddDays(1).ToString("yyyy-MM-dd"),
-           ["temperatureC"] = new OpenApiInteger(0),
+           ["temperatureC"] = 0,
-           ["temperatureF"] = new OpenApiInteger(32),
+           ["temperatureF"] = 32,
-           ["summary"] = new OpenApiString("Bracing"),
+           ["summary"] = "Bracing",
        };
    }
    return Task.CompletedTask;
});

Note that these changes are necessary even when only configuring the OpenAPI version to 3.0.

OpenAPI in YAML

ASP.NET now supports serving the generated OpenAPI document in YAML format. YAML can be more concise than JSON, eliminating curly braces and quotation marks when these can be inferred. YAML also supports multi-line strings, which can be useful for long descriptions.

To configure an app to serve the generated OpenAPI document in YAML format, specify the endpoint in the MapOpenApi call with a ".yaml" or ".yml" suffix, as shown in the following example:

app.MapOpenApi("/openapi/{documentName}.yaml");

Support for:

  • YAML is currently only available for the the OpenAPI served from the OpenAPI endpoint.
  • Generating OpenAPI documents in YAML format at build time is added in a future preview.

See this PR which added support for serving the generated OpenAPI document in YAML format.

Response description on ProducesResponseType for API controllers

The ProducesAttribute, ProducesResponseTypeAttribute, and ProducesDefaultResponseType attributes now accept an optional string parameter, Description, that will set the description of the response. Here's an example:

[HttpGet(Name = "GetWeatherForecast")]
[ProducesResponseType<IEnumerable<WeatherForecast>>(StatusCodes.Status200OK,
                   Description = "The weather forecast for the next 5 days.")]
public IEnumerable<WeatherForecast> Get()
{

And the generated OpenAPI:

        "responses": {
          "200": {
            "description": "The weather forecast for the next 5 days.",
            "content": {

Minimal APIs currently don't support ProducesResponseType.

Community contribution by Sander ten Brinke ๐Ÿ™

Populate XML doc comments into OpenAPI document

ASP.NET Core OpenAPI document generation will now include metadata from XML doc comments on method, class, and member definitions in the OpenAPI document. You must enable XML doc comments in your project file to use this feature. You can do this by adding the following property to your project file:

  <PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
  </PropertyGroup>

At build-time, the OpenAPI package will leverage a source generator to discover XML comments in the current application assembly and any project references and emit source code to insert them into the document via an OpenAPI document transformer.

Note that the C# build process does not capture XML doc comments placed on lambda expresions, so to use XML doc comments to add metadata to a minimal API endpoint, you must define the endpoint handler as a method, put the XML doc comments on the method, and then reference that method from the MapXXX method. For example, to use XML doc comments to add metadata to a minimal API endpoint originally defined as a lambda expression:

app.MapGet("/hello", (string name) =>$"Hello, {name}!");

Change the MapGet call to reference a method:

app.MapGet("/hello", Hello);

Define the Hello method with XML doc comments:

static partial class Program
{
    /// <summary>
    /// Sends a greeting.
    /// </summary>
    /// <remarks>
    /// Greeting a person by their name.
    /// </remarks>
    /// <param name="name">The name of the person to greet.</param>
    /// <returns>A greeting.</returns>
    public static string Hello(string name)
    {
        return $"Hello, {name}!";
    }
}

In the previous example the Hello method is added to the Program class, but you can add it to any class in your project.

The previous example illustrates the <summary>, <remarks>, and <param> XML doc comments. For more information about XML doc comments, including all the supported tags, see the C# documentation.

Since the core functionality is provided via a source generator, it can be disabled by adding the following MSBuild to your project file.

<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0-preview.2.*" GeneratePathProperty="true" />
</ItemGroup>

<Target Name="DisableCompileTimeOpenApiXmlGenerator" BeforeTargets="CoreCompile">
  <ItemGroup>
    <Analyzer Remove="$(PkgMicrosoft_AspNetCore_OpenApi)/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll" />
  </ItemGroup>
</Target>

The source generator process XML files included in the AdditionalFiles property. To add (or remove), sources modify the property as follows:

<Target Name="AddXmlSources" BeforeTargets="CoreCompile">
  <ItemGroup>
    <AdditionalFiles Include="$(PkgSome_Package)/lib/net10.0/Some.Package.xml" />
  </ItemGroup>
</Target>

Microsoft.AspNetCore.OpenApi added to the ASP.NET Core web API (Native AOT) template

The ASP.NET Core Web API (Native AOT) project template (short name webapiaot) now includes support for OpenAPI document generation using the Microsoft.AspNetCore.OpenApi package by default. This support is disabled by using the --no-openapi flag when creating a new project.

This was a community contribution by @sander1095. Thanks for this contribution!

Authentication and authorization

This section describes new features for authentication and authorization.

Authentication and authorization metrics

Metrics have been added for certain authentication and authorization events in ASP.NET Core. With this change, you can now obtain metrics for the following events:

  • Authentication:
    • Authenticated request duration
    • Challenge count
    • Forbid count
    • Sign in count
    • Sign out count
  • Authorization:
    • Count of requests requiring authorization

The following image shows an example of the Authenticated request duration metric in the Aspire dashboard:

Authenticated request duration in the Aspire dashboard

For more information, see ASP.NET Core Authorization and Authentication metrics.

Miscellaneous

This section describes miscellaneous new features in ASP.NET Core 10.0.

Better support for testing apps with top-level statements

.NET 10 now has better support for testing apps that use top-level statements. Previously developers had to manually add public partial class Program to the Program.cs file so that the test project could reference the Program class. public partial class Program was required because the top-level statement feature in C# 9 generated a Program class that was declared as internal.

In .NET 10, a source generator is used to generate the public partial class Program declaration if the programmer didn't declare it explicitly. Additionally, an analyzer was added to detect when public partial class Program is declared explicitly and advise the developer to remove it.

Image

The following PRs contribited to this feature:

Detect if URL is local using RedirectHttpResult.IsLocalUrl

Use the new RedirectHttpResult.IsLocalUrl(url) helper method to detect if a URL is local. A URL is considered local if the following are true:

URLs using virtual paths "~/" are also local.

IsLocalUrl is useful for validating URLs before redirecting to them to prevent open redirection attacks.

if (RedirectHttpResult.IsLocalUrl(url))
{
    return Results.LocalRedirect(url);
}

Thank you @martincostello for this contribution!


Additional resources