Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

This browser is no longer supported.

Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.

Download Microsoft EdgeMore info about Internet Explorer and Microsoft Edge
Table of contentsExit focus mode

What's new in ASP.NET Core in .NET 10

  • 2025-07-15
Feedback

In this article

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

This article will be updated as new preview releases are made available. For breaking changes, seeBreaking changes in .NET.

Blazor

This section describes new features for Blazor.

New and updated Blazor Web App security samples

We've added and updated the Blazor Web App security samples linked in the following articles:

All of our OIDC and Entra sample solutions now include a separate web API project (MinimalApiJwt) to demonstrate how to configure and call an external web API securely. Calling web APIs is demonstrated with a token handler and named HTTP client for an OIDC identity provider or Microsoft Identity Web packages/API for Microsoft Entra ID.

The sample solutions are configured in C# code in theirProgram files. To configure the solutions from app settings files (for example,appsettings.json) see thenewSupply configuration with the JSON configuration provider (app settings) section of the OIDC or Entra articles.

Our Entra article and sample apps also include new guidance on the following approaches:

QuickGridRowClass parameter

Apply a stylesheet class to a row of the grid based on the row item using the newRowClass parameter. In the following example, theGetRowCssClass 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, seeASP.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

NavigateTo no longer scrolls to the top for same-page navigations

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 aReconnectModal 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 thestyle-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'sReconnectModal 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 newcomponents-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, seeASP.NET Core Blazor SignalR guidance.

Ignore the query string and fragment when usingNavLinkMatch.All

TheNavLink component now ignores the query string and fragment when using theNavLinkMatch.All value for theMatch parameter. This means that the link retains theactive class if the URL path matches but the query string or fragment change. To revert to the original behavior, use theMicrosoft.AspNetCore.Components.Routing.NavLink.EnableMatchAllForQueryStringAndFragmentAppContext switch set totrue.

You can also override theShouldMatch method onNavLink to customize the matching behavior:

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

For more information, seeASP.NET Core Blazor routing and navigation.

CloseQuickGrid column options

You can now close theQuickGrid column options UI using the newHideColumnOptionsAsync method.

The following example uses theHideColumnOptionsAsync 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.HideColumnOptionsAsync())" />        </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 forHttpClient requests was opt-in. Now, response streaming is enabled by default.

This is a breaking change because callingHttpContent.ReadAsStreamAsync for anHttpResponseMessage.Content (response.Content.ReadAsStreamAsync()) returns aBrowserHttpReadStream and no longer aMemoryStream.BrowserHttpReadStream doesn't support synchronous operations, such asStream.Read(Span<Byte>). If your code uses synchronous operations, you can opt-out of response streaming or copy theStream into aMemoryStream yourself.

To opt-out of response streaming globally, use either of the following approaches:

  • Add the<WasmEnableStreamingResponse> property to the project file with a value offalse:

    <WasmEnableStreamingResponse>false</WasmEnableStreamingResponse>
  • Set theDOTNET_WASM_ENABLE_STREAMING_RESPONSE environment variable tofalse or0.

To opt-out of response streaming for an individual request, setSetBrowserResponseStreamingEnabled tofalse on theHttpRequestMessage (requestMessage in the following example):

requestMessage.SetBrowserResponseStreamingEnabled(false);

For more information, seeHttpClient andHttpRequestMessage with Fetch API request options (Call web API article).

Client-side fingerprinting

Last year, the release of .NET 9 introducedserver-side fingerprinting of static assets in Blazor Web Apps with the introduction ofMap Static Assets routing endpoint conventions (MapStaticAssets), theImportMap component, and theComponentBase.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 inindex.html with values computed during build to fingerprint static assets. A fingerprint is placed into theblazor.webassembly.js script file name.

The following markup must be present in thewwwwoot/index.html file to adopt the fingerprinting feature:

<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<OverrideHtmlAssetPlaceholders> property set totrue:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">  <PropertyGroup>    <TargetFramework>net10.0</TargetFramework>    <Nullable>enable</Nullable>    <ImplicitUsings>enable</ImplicitUsings>+   <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>  </PropertyGroup></Project>

Any script inindex.html with the fingerprint marker is fingerprinted by the framework. For example, a script file namedscripts.js in the app'swwwroot/js folder is fingerprinted by adding#[.{fingerprint}] before the file extension (.js):

<script src="js/scripts#[.{fingerprint}].js"></script>

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

TheProperties/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 toStaging:

<WasmApplicationEnvironmentName>Staging</WasmApplicationEnvironmentName>

The default environments are:

  • Development for build.
  • Production for publish.

Boot configuration file inlined

Blazor's boot configuration, which prior to the release of .NET 10 existed in a file namedblazor.boot.json, has been inlined into thedotnet.js script. This only affects developers who are interacting directly with theblazor.boot.json file, such as when developers are:

Currently, there's no documented replacement strategy for the preceding approaches. If you require either of the preceding strategies, open a new documentation issue describing your scenario using theOpen a documentation issue link at the bottom of either article.

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 thePersistentComponentState 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 thePersistentComponentState 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();    }}

State can be serialized for multiple components of the same type, and you can establish declarative state in a service for use around the app by callingRegisterPersistentService on the Razor components builder (AddRazorComponents) with a custom service type and render mode. For more information, seePrerender ASP.NET Core Razor components.

New JavaScript interop features

Blazor adds support for the following JS interop features:

  • Create an instance of a JS object using a constructor function and get theIJSObjectReference/IJSInProcessObjectReference .NET handle for referencing the instance.
  • Read or modify the value of a JS object property, both data and accessor properties.

The following asynchronous methods are available onIJSRuntime andIJSObjectReference with the same scoping behavior as the existingIJSRuntime.InvokeAsync method:

  • InvokeNewAsync(string identifier, object?[]? args): Invokes the specified JS constructor function asynchronously. The function is invoked with thenew operator. In the following example,jsInterop.TestClass is a class with a constructor function, andclassRef is anIJSObjectReference:

    var classRef = await JSRuntime.InvokeNewAsync("jsInterop.TestClass", "Blazor!");var text = await classRef.GetValueAsync<string>("text");var textLength = await classRef.InvokeAsync<int>("getTextLength");
  • GetValueAsync<TValue>(string identifier): Reads the value of the specified JS property asynchronously. The property can't be aset-only property. AJSException is thrown if the property doesn't exist. The following example returns a value from a data property:

    var valueFromDataPropertyAsync = await JSRuntime.GetValueAsync<int>(  "jsInterop.testObject.num");
  • SetValueAsync<TValue>(string identifier, TValue value): Updates the value of the specified JS property asynchronously. The property can't be aget-only property. If the property isn't defined on the target object, the property is created. AJSException is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example,num is created ontestObject with a value of 30 if it doesn't exist:

    await JSRuntime.SetValueAsync("jsInterop.testObject.num", 30);

Overloads are available for each of the preceding methods that take aCancellationToken argument orTimeSpan timeout argument.

The following synchronous methods are available onIJSInProcessRuntime andIJSInProcessObjectReference with the same scoping behavior as the existingIJSInProcessObjectReference.Invoke method:

  • InvokeNew(string identifier, object?[]? args): Invokes the specified JS constructor function synchronously. The function is invoked with thenew operator. In the following example,jsInterop.TestClass is a class with a constructor function, andclassRef is anIJSInProcessObjectReference:

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);var classRef = inProcRuntime.InvokeNew("jsInterop.TestClass", "Blazor!");var text = classRef.GetValue<string>("text");var textLength = classRef.Invoke<int>("getTextLength");
  • GetValue<TValue>(string identifier): Reads the value of the specified JS property synchronously. The property can't be aset-only property. AJSException is thrown if the property doesn't exist. The following example returns a value from a data property:

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);var valueFromDataProperty = inProcRuntime.GetValue<int>(  "jsInterop.testObject.num");
  • SetValue<TValue>(string identifier, TValue value): Updates the value of the specified JS property synchronously. The property can't be aget-only property. If the property isn't defined on the target object, the property is created. AJSException is thrown if the property exists but isn't writable or when a new property can't be added to the object. In the following example,num is created ontestObject with a value of 20 if it doesn't exist:

    var inProcRuntime = ((IJSInProcessRuntime)JSRuntime);inProcRuntime.SetValue("jsInterop.testObject.num", 20);

For more information, see the following sections of theCall JavaScript functions from .NET methods article:

Blazor WebAssembly performance profiling and diagnostic counters

New performance profiling and diagnostic counters are available for Blazor WebAssembly apps. For more information, see the following articles:

Preloaded Blazor framework static assets

In Blazor Web Apps, framework static assets are automatically preloaded usingLink headers, which allows the browser to preload resources before the initial page is fetched and rendered. In standalone Blazor WebAssembly apps, framework assets are scheduled for high priority downloading and caching early in browserindex.html page processing.

For more information, seeASP.NET Core Blazor static files.

NavigationManager.NavigateTo no longer throws aNavigationException

Previously, callingNavigationManager.NavigateTo during static server-side rendering (SSR) would throw aNavigationException, interrupting execution before being converted to a redirection response. This caused confusion during debugging and was inconsistent with interactive rendering, where code afterNavigateTo continues to execute normally.

CallingNavigationManager.NavigateTo during static SSR no longer throws aNavigationException. Instead, it behaves consistently with interactive rendering by performing the navigation without throwing an exception.

Code that relied onNavigationException being thrown should be updated. For example, in the default Blazor Identity UI, theIdentityRedirectManager previously threw anInvalidOperationException after callingRedirectTo to ensure it wasn't invoked during interactive rendering. This exception and the[DoesNotReturn] attributes should now be removed.

To revert to the previous behavior of throwing aNavigationException, set the followingAppContext switch:

AppContext.SetSwitch(    "Microsoft.AspNetCore.Components.Endpoints.NavigationManager.DisableThrowNavigationException",     isEnabled: false);

Blazor router has aNotFoundPage parameter

Blazor now provides an improved way to display a "Not Found" page when navigating to a non-existent page. You can specify a page to render whenNavigationManager.NotFound (described in the next section) is invoked by passing a page type to theRouter component using theNotFoundPage parameter. This approach is recommended over using theNotFound render fragment (<NotFound>...</NotFound>), as it supports routing, works across code Status Code Pages Re-execution Middleware, and is compatible even with non-Blazor scenarios. If both aNotFound render fragment andNotFoundPage are defined, the page specified byNotFoundPage takes priority.

<Router AppAssembly="@typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">    <Found Context="routeData">        <RouteView RouteData="@routeData" />        <FocusOnNavigate RouteData="@routeData" Selector="h1" />    </Found>    <NotFound>This content is ignored because NotFoundPage is defined.</NotFound></Router>

The Blazor project template now includes aNotFound.razor page by default. This page automatically renders wheneverNavigationManager.NotFound is called in your app, making it easier to handle missing routes with a consistent user experience.

For more information, seeASP.NET Core Blazor routing and navigation.

Not Found responses usingNavigationManager for static SSR and global interactive rendering

TheNavigationManager now includes aNotFound method to handle scenarios where a requested resource isn't found during static server-side rendering (static SSR) or global interactive rendering:

  • Static server-side rendering (static SSR): CallingNotFound sets the HTTP status code to 404.

  • Interactive rendering: Signals the Blazor router (Router component) to render Not Found content.

  • Streaming rendering: Ifenhanced navigation is active,streaming rendering renders Not Found content without reloading the page. When enhanced navigation is blocked, the framework redirects to Not Found content with a page refresh.

Streaming rendering can only render components that have a route, such as aNotFoundPage assignment (NotFoundPage="...") or aStatus Code Pages Re-execution Middleware page assignment (UseStatusCodePagesWithReExecute). The Not Found render fragment (<NotFound>...</NotFound>) and theDefaultNotFound 404 content ("Not found" plain text) don't have routes, so they can't be used during streaming rendering.

StreamingNavigationManager.NotFound content rendering uses (in order):

  • ANotFoundPage passed to theRouter component, if present.
  • A Status Code Pages Re-execution Middleware page, if configured.
  • No action if neither of the preceding approaches is adopted.

Non-streamingNavigationManager.NotFound content rendering uses (in order):

  • ANotFoundPage passed to theRouter component, if present.
  • Not Found render fragment content, if present.Not recommended in .NET 10 or later.
  • DefaultNotFound 404 content ("Not found" plain text).

Status Code Pages Re-execution Middleware withUseStatusCodePagesWithReExecute takes precedence for browser-based address routing problems, such as an incorrect URL typed into the browser's address bar or selecting a link that has no endpoint in the app.

You can use theNavigationManager.OnNotFound event for notifications whenNotFound is invoked.

For more information and examples, seeASP.NET Core Blazor routing and navigation.

Metrics and tracing

This release introduces comprehensive metrics and tracing capabilities for Blazor apps, providing detailed observability of the component lifecycle, navigation, event handling, and circuit management.

For more information, seeASP.NET Core Blazor performance best practices.

JavaScript bundler support

Blazor's build output isn't compatible with JavaScript bundlers, such asGulp,Webpack, andRollup. Blazor can now produce bundler-friendly output during publish by setting theWasmBundlerFriendlyBootConfig MSBuild property totrue.

For more information, seeHost and deploy ASP.NET Core Blazor.

Blazor WebAssembly static asset preloading in Blazor Web Apps

We replaced<link> headers with aLinkPreload component (<LinkPreload />) for preloading WebAssembly assets in Blazor Web Apps. This permits the app base path configuration (<base href="..." />) to correctly identify the app's root.

Removing the component disables the feature if the app is using aloadBootResource callback to modify URLs.

The Blazor Web App template adopts the feature by default in .NET 10, and apps upgrading to .NET 10 can implement the feature by placing theLinkPreload component after the base URL tag (<base>) in theApp component's head content (App.razor):

<head>    ...    <base href="/" />+   <LinkPreload />    ...</head>

For more information, seeHost and deploy ASP.NET Core server-side Blazor apps.

Improved form validation

Blazor now has improved form validation capabilities, including support for validating properties of nested objects and collection items.

To create a validated form, use aDataAnnotationsValidator component inside anEditForm component, just as before.

To opt into the new validation feature:

  1. Call theAddValidation extension method in theProgram file where services are registered.
  2. Declare the form model types in a C# class file, not in a Razor component (.razor).
  3. Annotate the root form model type with the[ValidatableType] attribute.

Without following the preceding steps, the validation behavior remains the same as in previous .NET releases.

The following example demonstrates customer orders with the improved form validation (details omitted for brevity):

InProgram.cs, callAddValidation on the service collection to enable the new validation behavior:

builder.Services.AddValidation();

In the followingOrder class, the[ValidatableType] attribute is required on the top-level model type. The other types are discovered automatically.OrderItem andShippingAddress aren't shown for brevity, but nested and collection validation works the same way in those types if they were shown.

Order.cs:

[ValidatableType]public class Order{    public Customer Customer { get; set; } = new();    public List<OrderItem> OrderItems { get; set; } = [];}public class Customer{    [Required(ErrorMessage = "Name is required.")]    public string? FullName { get; set; }    [Required(ErrorMessage = "Email is required.")]    public string? Email { get; set; }    public ShippingAddress ShippingAddress { get; set; } = new();}

In the followingOrderPage component, theDataAnnotationsValidator component is present in theEditForm component.

OrderPage.razor:

<EditForm Model="Model">    <DataAnnotationsValidator />    <h3>Customer Details</h3>    <div>        <label>            Full Name            <InputText @bind-Value="Model!.Customer.FullName" />        </label>        <ValidationMessage For="@(() => Model!.Customer.FullName)" />    </div>    @* ... form continues ... *@</EditForm>@code {    public Order? Model { get; set; }    protected override void OnInitialized() => Model ??= new();    // ... code continues ...}

The requirement to declare the model types outside of Razor components (.razor files) is due to the fact that both the new validation feature and the Razor compiler itself are using a source generator. Currently, output of one source generator can't be used as an input for another source generator.

Custom Blazor cache andBlazorCacheBootResources MSBuild property removed

Now that all Blazor client-side files are fingerprinted and cached by the browser, Blazor's custom caching mechanism and theBlazorCacheBootResources MSBuild property have been removed from the framework. If the client-side project's project file contains the MSBuild property, remove the property, as it no longer has any effect:

- <BlazorCacheBootResources>...</BlazorCacheBootResources>

For more information, seeASP.NET Core Blazor WebAssembly caching and integrity check failures.

Web Authentication API (passkey) support for ASP.NET Core Identity

Web Authentication (WebAuthn) API support, known widely aspasskeys, is a modern, phishing-resistant authentication method that improves security and user experience by leveraging public key cryptography and device-based authentication. ASP.NET Core Identity now supports passkey authentication based on WebAuthn and FIDO2 standards. This feature allows users to sign in without passwords, using secure, device-based authentication methods, such as biometrics or security keys.

The Preview 6 Blazor Web App project template provides out-of-the-box passkey management and login functionality.

Migration guidance for existing apps will be published for the upcoming release of Preview 7, which is scheduled for mid-August.

Circuit state persistence

During server-side rendering, Blazor Web Apps can now persist a user's session (circuit) state when the connection to the server is lost for an extended period of time or proactively paused, as long as a full-page refresh isn't triggered. This allows users to resume their session without losing unsaved work in the following scenarios:

  • Browser tab throttling
  • Mobile device users switching apps
  • Network interruptions
  • Proactive resource management (pausing inactive circuits)

Enhanced navigation with circuit state persistence isn't currently supported but planned for a future release.

Persisting state requires fewer server resources than persisting circuits:

  • Even if disconnected, a circuit might continue to perform work and consume CPU, memory, and other resources. Persisted state only consumes a fixed amount of memory that the developer controls.
  • Persisted state represents a subset of the memory consumed by the app, so the server isn't required to keep track of the app's components and other server-side objects.

State is persisted for two scenarios:

  • Component state: State that components use for Interactive Server rendering, for example, a list of items retrieved from the database or a form that the user is filling out.
  • Scoped services: State held inside of a server-side service, for example, the current user.

Conditions:

  • The feature is only effective for Interactive Server rendering.
  • If the user refreshes the page (app), the persisted state is lost.
  • The state must be JSON serializable. Cyclic references or ORM entities may not serialize correctly.
  • Use@key for uniqueness when rendering components in a loop to avoid key conflicts.
  • Persist only necessary state. Storing excessive data may impact performance.
  • No automatic hibernation. You must opt-in and configure state persistence explicitly.
  • No guarantee of recovery. If state persistence fails, the app falls back to the default disconnected experience.

State persistence is enabled by default whenAddInteractiveServerComponents is called onAddRazorComponents in theProgram file.MemoryCache is the default storage implementation for single app instances and stores up to 1,000 persisted circuits for two hours, which are configurable.

Use the following options to change the default values of the in-memory provider:

  • PersistedCircuitInMemoryMaxRetained ({CIRCUIT COUNT} placeholder): The maximum number of circuits to retain. The default is 1,000 circuits. For example, use2000 to retain state for up to 2,000 circuits.
  • PersistedCircuitInMemoryRetentionPeriod ({RETENTION PERIOD} placeholder): The maximum retention period as aTimeSpan. The default is two hours. For example, useTimeSpan.FromHours(3) for a three-hour retention period.
services.Configure<CircuitOptions>(options =>{    options.PersistedCircuitInMemoryMaxRetained = {CIRCUIT COUNT};    options.PersistedCircuitInMemoryRetentionPeriod = {RETENTION PERIOD};});

Persisting component state across circuits is built on top of the existingPersistentComponentState API, which continues to persist state for prerendered components that adopt an interactive render mode.

[NOTE]Persisting component state for prerendering works for any interactive render mode, but circuit state persistence only works for theInteractive Server render mode.

Annotate component properties with[SupplyFromPersistentComponentState] to enable circuit state persistence. The following example also keys the items with the@key directive attribute to provide a unique identifier for each component instance:

@foreach (var item in Items){    <ItemDisplay @key="@($"unique-prefix-{item.Id}")" Item="item" />}@code {    [SupplyFromPersistentComponentState]    public List<Item> Items { get; set; }    protected override async Task OnInitializedAsync()    {        Items ??= await LoadItemsAsync();    }}

To persist State for scoped services, annotate service properties with[SupplyFromPersistentComponentState], add the service to the service collection, and call theRegisterPersistentService extension method with the service:

public class CustomUserService{    [SupplyFromPersistentComponentState]    public string UserData { get; set; }}services.AddScoped<CustomUserService>();services.AddRazorComponents()  .AddInteractiveServerComponents()  .RegisterPersistentService<CustomUserService>(RenderMode.InteractiveAuto);

[NOTE]The preceding example persistsUserData state when the service is used in component prerendering for both Interactive Server and Interactive WebAssembly rendering becauseRenderMode.InteractiveAuto is specified toRegisterPersistentService. However, circuit state persistence is only available for theInteractive Server render mode.

For more information, seePrerender ASP.NET Core Razor components.

To handle distributed state persistence (and to act as the default state persistence mechanism when configured), assign aHybridCache (API:HybridCache) to the app, which configures its own persistence period (PersistedCircuitDistributedRetentionPeriod, eight hours by default).HybridCache is used because it provides a unified approach to distributed storage that doesn't require separate packages for each storage provider.

In the following example, aHybridCache is implemented with theRedis storage provider:

services.AddHybridCache()    .AddRedis("{CONNECTION STRING}");services.AddRazorComponents()    .AddInteractiveServerComponents();

In the preceding example, the{CONNECTION STRING} placeholder represents the Redis cache connection string, which should be provided using a secure approach, such as theSecret Manager tool in the Development environment orAzure Key Vault withAzure Managed Identities for Azure-deployed apps in any environment.

To proactively pause and resume circuits in custom resource management scenarios, callBlazor.pauseCircuit andBlazor.resumeCircuit from a JavaScript event handler. In the following example, changes in the the visibility of the app either pause or resume the user's circuit:

window.addEventListener('visibilitychange', () => {  if (document.visibilityState === 'hidden') {    Blazor.pauseCircuit();  } else if (document.visibilityState === 'visible') {    Blazor.resumeCircuit();  }});

Note

Support for circuit state persistence with enhanced navigation is planned for a future release.

The following API renaming is planned for the upcoming Preview 7 release in August:

[SupplyFromPersistentComponentState] will be renamed to[PersistentState].Blazor.pauseCircuit will be renamed toBlazor.pause.Blazor.resumeCircuit will be renamed toBlazor.resume.

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 tonull 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 theDataAnnotations 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 theAddValidation 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 theDisableValidation 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();

Note

Several small improvements and fixes have been made to the Minimal APIs validation generator introduced in ASP.NET Core for .NET 10. To support future enhancements, the underlying validation resolver APIs are now marked as experimental. The top-levelAddValidation APIs and the built-in validation filter remain stable and non-experimental.

Validation with record types

Minimal APIs also support validation with C# record types. Record types can be validated using attributes from theSystem.ComponentModel.DataAnnotations namespace, similar to classes. For example:

public record Product(    [Required] string Name,    [Range(1, 1000)] int Quantity);

When using record types as parameters in Minimal API endpoints, validation attributes are automatically applied in the same way as class types:

app.MapPost("/products", (Product product) =>{    // Endpoint logic here    return TypedResults.Ok(product);});

Minimal API Validation integration with IProblemDetailsService

Error responses from the validation logic for minimal APIs can now be customized by anIProblemDetailsService implementation provided in the application services collection (Dependency Injection container). This enables more consistent and user-specific error responses.

Support for Server-Sent Events (SSE)

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

TheTypedResults class has a new static method calledServerSentEvents that can be used to return aServerSentEvents result. The first parameter to this method is anIAsyncEnumerable<SseItem<T>> that represents the stream of event messages to be sent to the client.

The following example illustrates how to use theTypedResults.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 theTypedResults.ServerSentEvents API to return a stream of heart rate events as string,ServerSentEvents, and JSON objects to the client.
  • Controller API sample app using theTypedResults.ServerSentEvents API to return a stream of heart rate events as string,ServerSentEvents, and JSON objects to the client.

Validation APIs moved to Microsoft.Extensions.Validation

The validation APIs have moved to theMicrosoft.Extensions.Validation namespace and NuGet package. This change makes the APIs usable outside of ASP.NET Core HTTP scenarios. The public APIs and behavior remain unchanged—only the package and namespace are different. Existing projects don't require code changes, as old references redirect to the new implementation.

OpenAPI

This section describes new features for OpenAPI.

OpenAPI 3.1 support

ASP.NET Core has added support for generatingOpenAPI 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 forJSON Schema draft 2020-12.

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

  • Nullable types no longer have thenullable: true property in the schema.
  • Instead of anullable: true property, they have atype keyword whose value is an array that includesnull as one of the types.
  • Properties or parameters defined as a C#int orlong now appear in the generated OpenAPI document without thetype: integer fieldand have apattern field limiting the value to digits.This happens when theNumberHandling property in theJsonSerializerOptions is set toAllowReadingFromString, the default for ASP.NET Core Web apps. To enable C#int andlong to be represented in the OpenAPI document astype: integer, set theNumberHandling property toStrict.

With this feature, the default OpenAPI version for generated documents is3.1. The version can be changed by explicitly setting theOpenApiVersion property of theOpenApiOptions in theconfigureOptions delegate parameter ofAddOpenApi:

builder.Services.AddOpenApi(options =>{    options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1;});

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

<PropertyGroup>    <TargetFramework>net10.0</TargetFramework>    <Nullable>enable</Nullable>    <ImplicitUsings>enable</ImplicitUsings>    <OpenApiGenerateDocuments>true</OpenApiGenerateDocuments>    <!-- Configure build-time OpenAPI generation to produce an OpenAPI 3.1 document. -->    <OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_1</OpenApiGenerateDocumentsOptions></PropertyGroup>

OpenAPI 3.1 support was primarily added in the followingPR.

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, anIOpenApiSchema can either be an inlinedOpenApiSchema or anOpenApiSchemaReference that points to a schema defined elsewhere in the document.
  • TheNullable property has been removed from theOpenApiSchema type. To determine if a type is nullable, evaluate if theOpenApiSchema.Type property setsJsonSchemaType.Null.

One of the most significant changes is that theOpenApiAny class has been dropped in favor of usingJsonNode directly. Transformers that useOpenApiAny need to be updated to useJsonNode. 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:

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

Support for:

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

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

Response description on ProducesResponseType for API controllers

TheProducesAttribute,ProducesResponseTypeAttribute, andProducesDefaultResponseType 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 supportProducesResponseType.

Community contribution bySander 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 theMapXXX 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 theMapGet call to reference a method:

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

Define theHello 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 theHello method is added to theProgram 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 theC# 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 theAdditionalFiles 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

TheASP.NET Core Web API (Native AOT) project template (short namewebapiaot) now includes support for OpenAPI document generation using theMicrosoft.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!

Support for IOpenApiDocumentProvider in the DI container.

ASP.NET Core in .NET 10 supportsIOpenApiDocumentProvider in the dependency injection (DI) container. Developers can injectIOpenApiDocumentProvider into their apps and use it to access the OpenAPI document. This approach is useful for accessing OpenAPI documents outside the context of HTTP requests, such as in background services or custom middleware.

Previously, running application startup logic without launching an HTTP server could be done by usingHostFactoryResolver with a no-opIServer implementation. The new feature simplifies this process by providing a streamlined API inspired by Aspire'sIDistributedApplicationPublisher, which is part of Aspire's framework for distributed application hosting and publishing.

For more information, seedotnet/aspnetcore #61463.

Improvements to the XML comment generator

XML comment generation handles complex types in .NET 10 better than earlier versions of .NET.

  • It produces accurate and complete XML comments for a wider range of types.
  • It handles more complex scenarios.
  • It gracefully bypasses processing for complex types that cause build errors in earlier versions.

These improvements change the failure mode for certain scenarios from build errors to missing metadata.

In addition, XML doc comment processing can now be configured to access XML comments in other assemblies. This is useful for generating documentation for types that are defined outside the current assembly, such as theProblemDetails type in theMicrosoft.AspNetCore.Http namespace.

This configuration is done with directives in the project build file. The following example shows how to configure the XML comment generator to access XML comments for types in theMicrosoft.AspNetCore.Http assembly, which includes theProblemDetails class.

<Target Name="AddOpenApiDependencies" AfterTargets="ResolveReferences">  <ItemGroup>  <!-- Include XML documentation from Microsoft.AspNetCore.Http.Abstractions    to get metadata for ProblemDetails -->    <AdditionalFiles          Include="@(ReferencePath->'            %(RootDir)%(Directory)%(Filename).xml')"          Condition="'%(ReferencePath.Filename)' ==           'Microsoft.AspNetCore.Http.Abstractions'"          KeepMetadata="Identity;HintPath" />  </ItemGroup></Target>

We expect to include XML comments from a selected set of assemblies in the shared framework in future previews, to avoid the need for this configuration in most cases.

Support for generating OpenApiSchemas in transformers

Developers can now generate a schema for a C# type using the same logic as ASP.NET Core OpenAPI document generation and add it to the OpenAPI document. The schema can then be referenced from elsewhere in the OpenAPI document.

The context passed to document, operation, and schema transformers includes a newGetOrCreateSchemaAsync method that can be used to generate a schema for a type.This method also has an optionalApiParameterDescription parameter to specify additional metadata for the generated schema.

To support adding the schema to the OpenAPI document, aDocument property has been added to the Operation and Schema transformer contexts. This allows any transformer to add a schema to the OpenAPI document using the document'sAddComponent method.

Example

To use this feature in a document, operation, or schema transformer, create the schema using theGetOrCreateSchemaAsync method provided in the context and add it to the OpenAPI document using the document'sAddComponent method.

builder.Services.AddOpenApi(options =>{    options.AddOperationTransformer(async (operation, context, cancellationToken) =>    {        // Generate schema for error responses        var errorSchema = await context.GetOrCreateSchemaAsync(typeof(ProblemDetails), null, cancellationToken);        context.Document?.AddComponent("Error", errorSchema);        operation.Responses ??= new OpenApiResponses();        // Add a "4XX" response to the operation with the newly created schema        operation.Responses["4XX"] = new OpenApiResponse        {            Description = "Bad Request",            Content = new Dictionary<string, OpenApiMediaType>            {                ["application/problem+json"] = new OpenApiMediaType                {                    Schema = new OpenApiSchemaReference("Error", context.Document)                }            }        };    });});

OpenAPI.NET updated to Preview.18

The OpenAPI.NET library used in ASP.NET Core OpenAPI document generation was upgraded tov2.0.0-preview18. The v2.0.0-preview18 version improves compatibility with the updated library version.

The previous v2.0.0-preview17 version included a number of bug fixes and improvements and also introduced some breaking changes. The breaking changes should only affect users that use document, operation, or schema transformers. Breaking changes in this version that may affect developers include the following:

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, seeASP.NET Core Authorization and Authentication metrics.

Miscellaneous

This section describes miscellaneous new features in .NET 10.

Automatic eviction from memory pool

The memory pools used by Kestrel, IIS, and HTTP.sys now automatically evict memory blocks when the application is idle or under less load. The feature runs automatically and doesn't need to be enabled or configured manually.

Why memory eviction matters

Previously, memory allocated by the pool would remain reserved, even when not in use. This feature releases memory back to the system when the app is idle for a period of time. This eviction reduces overall memory usage and helps applications stay responsive under varying workloads.

Use memory eviction metrics

Metrics have been added to the default memory pool used by our server implementations. The new metrics are under the name"Microsoft.AspNetCore.MemoryPool".

For information about metrics and how to use them, seeASP.NET Core metrics.

Manage memory pools

Besides using memory pools more efficiently by evicting unneeded memory blocks, .NET 10 improves the experience of creating memory pools. It does this by providing a built-inIMemoryPoolFactory and aMemoryPoolFactory implementation. It makes the implementation available to your application through dependency injection.

The following code example shows a simple background service that uses the built-in memory pool factory implementation to create memory pools. These pools benefit from the automatic eviction feature:

public class MyBackgroundService : BackgroundService{    private readonly MemoryPool<byte> _memoryPool;    public MyBackgroundService(IMemoryPoolFactory<byte> factory)    {        _memoryPool = factory.Create();    }    protected override async Task ExecuteAsync(CancellationToken stoppingToken)    {        while (!stoppingToken.IsCancellationRequested)        {            try            {                await Task.Delay(20, stoppingToken);                // do work that needs memory                var rented = _memoryPool.Rent(100);                rented.Dispose();            }            catch (OperationCanceledException)            {                return;            }        }    }}

To use your own memory pool factory, make a class that implementsIMemoryPoolFactory and register it with dependency injection, as the following example does. Memory pools created this way also benefit from the automatic eviction feature:

services.AddSingleton<IMemoryPoolFactory<byte>,CustomMemoryPoolFactory>();public class CustomMemoryPoolFactory : IMemoryPoolFactory<byte>{    public MemoryPool<byte> Create()    {        // Return a custom MemoryPool implementation        // or the default, as is shown here.        return MemoryPool<byte>.Shared;    }}

Customizable security descriptors for HTTP.sys

You can now specify a custom security descriptor for HTTP.sys request queues. The newRequestQueueSecurityDescriptor property onHttpSysOptions enables more granular control over access rights for the request queue. This granular control lets you tailor security to your application's needs.

What you can do with the new property

Arequest queue in HTTP.sys is a kernel-level structure that temporarily stores incoming HTTP requests until your application is ready to process them. By customizing the security descriptor, you can allow or deny specific users or groups access to the request queue. This is useful in scenarios where you want to restrict or delegate HTTP.sys request handling at the operating system level.

How to use the new property

TheRequestQueueSecurityDescriptor property applies only when creating a new request queue. The property doesn't affect existing request queues. To use this property, set it to aGenericSecurityDescriptor instance when configuring your HTTP.sys server.

For example, the following code allows all authenticated users but denies guests:

using System.Security.AccessControl;using System.Security.Principal;using Microsoft.AspNetCore.Server.HttpSys;// Create a new security descriptorvar securityDescriptor = new CommonSecurityDescriptor(isContainer: false, isDS: false, sddlForm: string.Empty);// Create a discretionary access control list (DACL)var dacl = new DiscretionaryAcl(isContainer: false, isDS: false, capacity: 2);dacl.AddAccess(    AccessControlType.Allow,    new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null),    -1,    InheritanceFlags.None,    PropagationFlags.None);dacl.AddAccess(    AccessControlType.Deny,    new SecurityIdentifier(WellKnownSidType.BuiltinGuestsSid, null),    -1,    InheritanceFlags.None,    PropagationFlags.None);// Assign the DACL to the security descriptorsecurityDescriptor.DiscretionaryAcl = dacl;// Configure HTTP.sys optionsvar builder = WebApplication.CreateBuilder();builder.WebHost.UseHttpSys(options =>{    options.RequestQueueSecurityDescriptor = securityDescriptor;});

For more information, seeHTTP.sys web server implementation in ASP.NET Core.

Better support for testing apps with top-level statements

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

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

Image

The following PRs contribited to this feature:

New JSON Patch implementation withSystem.Text.Json

JSON Patch:

  • Is a standard format for describing changes to apply to a JSON document.
  • Is defined in RFC 6902 and is widely used in RESTful APIs to perform partial updates to JSON resources.
  • Represents a sequence of operations (for example, Add, Remove, Replace, Move, Copy, Test) that can be applied to modify a JSON document.

In web apps, JSON Patch is commonly used in a PATCH operation to perform partial updates of a resource. Rather than sending the entire resource for an update, clients can send a JSON Patch document containing only the changes. Patching reduces payload size and improves efficiency.

This release introduces a new implementation ofMicrosoft.AspNetCore.JsonPatch based onSystem.Text.Json serialization. This feature:

  • Aligns with modern .NET practices by leveraging theSystem.Text.Json library, which is optimized for .NET.
  • Provides improved performance and reduced memory usage compared to the legacyNewtonsoft.Json-based implementation.

The following benchmarks compare the performance of the newSystem.Text.Json implementation with the legacyNewtonsoft.Json implementation.

ScenarioImplementationMeanAllocated Memory
Application BenchmarksNewtonsoft.JsonPatch271.924 µs25 KB
System.Text.JsonPatch1.584 µs3 KB
Deserialization BenchmarksNewtonsoft.JsonPatch19.261 µs43 KB
System.Text.JsonPatch7.917 µs7 KB

These benchmarks highlight significant performance gains and reduced memory usage with the new implementation.

Notes:

  • The new implementation isn't a drop-in replacement for the legacy implementation. In particular, the new implementation doesn't support dynamic types, for example,ExpandoObject.
  • The JSON Patch standard hasinherent security risks. Since these risks are inherent to the JSON Patch standard, the new implementationdoesn't attempt to mitigate inherent security risks. It's the responsibility of the developer to ensure that the JSON Patch document is safe to apply to the target object. For more information, see theMitigating Security Risks section.

Usage

To enable JSON Patch support withSystem.Text.Json, install theMicrosoft.AspNetCore.JsonPatch.SystemTextJson NuGet package.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease

This package provides aJsonPatchDocument<T> class to represent a JSON Patch document for objects of typeT and custom logic for serializing and deserializing JSON Patch documents usingSystem.Text.Json. The key method of theJsonPatchDocument<T> class isApplyTo, which applies the patch operations to a target object of typeT.

The following examples demonstrate how to use theApplyTo method to apply a JSON Patch document to an object.

Example: Applying aJsonPatchDocument

The following example demonstrates:

  1. Theadd,replace, andremove operations.
  2. Operations on nested properties.
  3. Adding a new item to an array.
  4. Using a JSON String Enum Converter in a JSON Patch document.
// Original objectvar person = new Person {  FirstName = "John",  LastName = "Doe",  Email = "johndoe@gmail.com",  PhoneNumbers = [new() {Number = "123-456-7890", Type = PhoneNumberType.Mobile}],  Address = new Address  {    Street = "123 Main St",    City = "Anytown",    State = "TX"  }};// Raw JSON Patch documentvar jsonPatch = """[  { "op": "replace", "path": "/FirstName", "value": "Jane" },  { "op": "remove", "path": "/Email"},  { "op": "add", "path": "/Address/ZipCode", "value": "90210" },  {    "op": "add",    "path": "/PhoneNumbers/-",    "value": { "Number": "987-654-3210", "Type": "Work" }  }]""";// Deserialize the JSON Patch documentvar patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);// Apply the JSON Patch documentpatchDoc!.ApplyTo(person);// Output updated objectConsole.WriteLine(JsonSerializer.Serialize(person, serializerOptions));// Output:// {//   "firstName": "Jane",//   "lastName": "Doe",//   "address": {//     "street": "123 Main St",//     "city": "Anytown",//     "state": "TX",//     "zipCode": "90210"//   },//   "phoneNumbers": [//     {//       "number": "123-456-7890",//       "type": "Mobile"//     },//     {//       "number": "987-654-3210",//       "type": "Work"//     }//   ]// }

TheApplyTo method generally follows the conventions and options ofSystem.Text.Json for processing theJsonPatchDocument, including the behavior controlled by the following options:

  • NumberHandling: Whether numeric properties are read from strings.
  • PropertyNameCaseInsensitive: Whether property names are case-sensitive.

Key differences betweenSystem.Text.Json and the newJsonPatchDocument<T> implementation:

  • The runtime type of the target object, not the declared type, determines which propertiesApplyTo patches.
  • System.Text.Json deserialization relies on the declared type to identify eligible properties.

Example: Applying aJsonPatchDocument with error handling

There are various errors that can occur when applying a JSON Patch document. For example, the target object may not have the specified property, or the value specified might be incompatible with the property type.

JSON Patch also supports thetest operation. Thetest operation checks if a specified value is equal to the target property, and if not, returns an error.

The following example demonstrates how to handle these errors gracefully.

Important

The object passed to theApplyTo method is modified in place. It is the caller's responsiblity to discard these changes if any operation fails.

// Original objectvar person = new Person {  FirstName = "John",  LastName = "Doe",  Email = "johndoe@gmail.com"};// Raw JSON Patch documentvar jsonPatch = """[  { "op": "replace", "path": "/Email", "value": "janedoe@gmail.com"},  { "op": "test", "path": "/FirstName", "value": "Jane" },  { "op": "replace", "path": "/LastName", "value": "Smith" }]""";// Deserialize the JSON Patch documentvar patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);// Apply the JSON Patch document, catching any errorsDictionary<string, string[]>? errors = null;patchDoc!.ApplyTo(person, jsonPatchError =>    {        errors ??= new ();        var key = jsonPatchError.AffectedObject.GetType().Name;        if (!errors.ContainsKey(key))        {            errors.Add(key, new string[] { });        }        errors[key] = errors[key].Append(jsonPatchError.ErrorMessage).ToArray();    });if (errors != null){    // Print the errors    foreach (var error in errors)    {        Console.WriteLine($"Error in {error.Key}: {string.Join(", ", error.Value)}");    }}// Output updated objectConsole.WriteLine(JsonSerializer.Serialize(person, serializerOptions));// Output:// Error in Person: The current value 'John' at path 'FirstName' is not equal // to the test value 'Jane'.// {//   "firstName": "John",//   "lastName": "Smith",              <<< Modified!//   "email": "janedoe@gmail.com",     <<< Modified!//   "phoneNumbers": []// }

Mitigating security risks

When using theMicrosoft.AspNetCore.JsonPatch.SystemTextJson package, it's critical to understand and mitigate potential security risks. The following sections outline the identified security risks associated with JSON Patch and provide recommended mitigations to ensure secure usage of the package.

Important

This is not an exhaustive list of threats. App developers must conduct their own threat model reviews to determine an app-specific comprehensive list and come up with appropriate mitigations as needed. For example, apps which expose collections to patch operations should consider the potential for algorithmic complexity attacks if those operations insert or remove elements at the beginning of the collection.

By running comprehensive threat models for their own apps and addressing identified threats while following the recommended mitigations below, consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks.

Consumers of these packages can integrate JSON Patch functionality into their apps while minimizing security risks, including:

  • Run comprehensive threat models for their own apps.
  • Address identified threats.
  • Follow the recommended mitigations in the following sections.
Denial of Service (DoS) via memory amplification
  • Scenario: A malicious client submits acopy operation that duplicates large object graphs multiple times, leading to excessive memory consumption.
  • Impact: Potential Out-Of-Memory (OOM) conditions, causing service disruptions.
  • Mitigation:
    • Validate incoming JSON Patch documents for size and structure before callingApplyTo.
    • The validation must be app specific, but an example validation can look similar to the following:
public void Validate(JsonPatchDocument<T> patch){    // This is just an example. It's up to the developer to make sure that    // this case is handled properly, based on the app's requirements.    if (patch.Operations.Where(op=>op.OperationType == OperationType.Copy).Count()        > MaxCopyOperationsCount)    {        throw new InvalidOperationException();    }}
Business Logic Subversion
  • Scenario: Patch operations can manipulate fields with implicit invariants, (for example, internal flags, IDs, or computed fields), violating business constraints.
  • Impact: Data integrity issues and unintended app behavior.
  • Mitigation:
    • Use POCO objects with explicitly defined properties that are safe to modify.
    • Avoid exposing sensitive or security-critical properties in the target object.
    • If no POCO object is used, validate the patched object after applying operations to ensure business rules and invariants aren't violated.
Authentication and authorization
  • Scenario: Unauthenticated or unauthorized clients send malicious JSON Patch requests.
  • Impact: Unauthorized access to modify sensitive data or disrupt app behavior.
  • Mitigation:
    • Protect endpoints accepting JSON Patch requests with proper authentication and authorization mechanisms.
    • Restrict access to trusted clients or users with appropriate permissions.

Detect if URL is local usingRedirectHttpResult.IsLocalUrl

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

URLs usingvirtual paths"~/" are also local.

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

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

Thank you@martincostello for this contribution!

Related content

HTTP.sys web server implementation in ASP.NET Core

Collaborate with us on GitHub
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, seeour contributor guide.

Feedback

Was this page helpful?

YesNo

In this article

Was this page helpful?

YesNo