Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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. For breaking changes, see Breaking changes in .NET.
Blazor
This section describes new features for Blazor.
New and updated Blazor Web App security samples
We've added and updated Blazor Web App security samples cross-linked in the following articles:
- Secure an ASP.NET Core Blazor Web App with OpenID Connect (OIDC)
- Secure an ASP.NET Core Blazor Web App with Microsoft Entra ID
- Secure an ASP.NET Core Blazor Web App with Windows Authentication
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 their Program
files. To configure the solutions from app settings files (for example, appsettings.json
) see the new Supply configuration with the JSON configuration provider (app settings) section of the OIDC or Entra articles.
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:
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 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 HideColumnOptionsAsync
method.
The following example uses the HideColumnOptionsAsync
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 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 markup must be present in the wwwwoot/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 to true
:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+ <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
</Project>
Any script in index.html
with the fingerprint marker is fingerprinted by the framework. For example, a script file named scripts.js
in the app's wwwroot/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
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 inlined
Blazor's boot configuration, which prior to the release of .NET 10 existed in a file named blazor.boot.json
, has been inlined into the dotnet.js
script. This only affects developers who are interacting directly with the blazor.boot.json
file, such as when developers are:
- Checking file integrity for published assets with the troubleshoot integrity PowerShell script per the guidance in ASP.NET Core Blazor WebAssembly .NET bundle caching and integrity check failures.
- Changing the file name extension of DLL files when not using the default Webcil file format per the guidance in Host and deploy ASP.NET Core Blazor WebAssembly.
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 the Open 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 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();
}
}
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 calling RegisterPersistentService
on the Razor components builder (AddRazorComponents) with a custom service type and render mode. For more information, see Prerender 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 the IJSObjectReference/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 on IJSRuntime and IJSObjectReference with the same scoping behavior as the existing IJSRuntime.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 an IJSObjectReference: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. A JSException 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. A JSException 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 a CancellationToken argument or TimeSpan timeout argument.
The following synchronous methods are available on IJSInProcessRuntime and IJSInProcessObjectReference with the same scoping behavior as the existing IJSInProcessObjectReference.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 an IJSInProcessObjectReference: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. A JSException 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. A JSException 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 the Call JavaScript functions from .NET methods article:
- Create an instance of a JS object using a constructor function
- Read or modify the value of a JS object property
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:
- ASP.NET Core Blazor WebAssembly browser developer tools diagnostics
- ASP.NET Core Blazor WebAssembly Event Pipe diagnostics
Preloaded Blazor framework static assets
In Blazor Web Apps, framework static assets are automatically preloaded using Link
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 browser index.html
page processing.
For more information, see ASP.NET Core Blazor static files.
NavigationManager.NavigateTo
no longer throws a NavigationException
Previously, calling NavigationManager.NavigateTo during static server-side rendering (SSR) would throw a NavigationException, interrupting execution before being converted to a redirection response. This caused confusion during debugging and was inconsistent with interactive rendering, where code after NavigateTo continues to execute normally.
Calling NavigationManager.NavigateTo during static SSR no longer throws a NavigationException. Instead, it behaves consistently with interactive rendering by performing the navigation without throwing an exception.
Code that relied on NavigationException being thrown should be updated. For example, in the default Blazor Identity UI, the IdentityRedirectManager
previously threw an InvalidOperationException after calling RedirectTo
to ensure it wasn't invoked during interactive rendering. This exception and the [DoesNotReturn]
attributes should now be removed.
Not Found responses using NavigationManager
for static SSR and global interactive rendering
The NavigationManager now includes a NotFound
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): Calling
NotFound
sets the HTTP status code to 404. - Streaming rendering: Throws an exception if the response has already started.
- Interactive rendering: Signals the Blazor router (
Router
component) to render Not Found content.
Per-page/component rendering support is planned for Preview 5 in June, 2025.
You can use the NavigationManager.OnNotFound
event for notifications when NotFound
is invoked.
For more information and examples, see ASP.NET Core Blazor routing and navigation.
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:
- .NET MAUI Blazor Hybrid and Web App with ASP.NET Core Identity
MauiBlazorWebIdentity
sample app (dotnet/blazor-samples
GitHub repository)
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:
- Creating custom
[Validation]
attribute implementations. - Implementing the
IValidatableObject
interface for complex validation logic.
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();
Validation with record types
Minimal APIs also support validation with C# record types. Record types can be validated using attributes from the System.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);
});
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 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
field and have apattern
field limiting the value to digits. This happens when the NumberHandling property in the JsonSerializerOptions 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 the NumberHandling property toStrict
.
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 =>
{
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 the OpenApiGenerateDocumentsOptions
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 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 inlinedOpenApiSchema
or anOpenApiSchemaReference
that points to a schema defined elsewhere in the document. - The
Nullable
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 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:
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.
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!
Support for IOpenApiDocumentProvider in the DI container.
ASP.NET Core in .NET 10 supports IOpenApiDocumentProvider in the dependency injection (DI) container. Developers can inject IOpenApiDocumentProvider
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 using HostFactoryResolver
with a no-op IServer
implementation. The new feature simplifies this process by providing a streamlined API inspired by Aspire's IDistributedApplicationPublisher, which is part of Aspire's framework for distributed application hosting and publishing.
For more information, see dotnet/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 the ProblemDetails
type in the Microsoft.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 the Microsoft.AspNetCore.Http
assembly, which includes the ProblemDetails
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 new GetOrCreateSchemaAsync
method that can be used to generate a schema for a type.
This method also has an optional ApiParameterDescription
parameter to specify additional metadata for the generated schema.
To support adding the schema to the OpenAPI document, a Document
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's AddComponent
method.
Example
To use this feature in a document, operation, or schema transformer, create the schema using the GetOrCreateSchemaAsync
method provided in the context and add it to the OpenAPI document using the document's AddComponent
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.17
The OpenAPI.NET library used in ASP.NET Core OpenAPI document generation was upgraded to v2.0.0-preview17. This version includes a number of bug fixes and improvements and also introduces some breaking changes. The breaking changes should only affect users that use document, operation, or schema transformers. Breaking changes in this iteration 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:
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.
The following PRs contribited to this feature:
New JsonPatch Implementation with System.Text.Json
- 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 of JsonPatch
based on System.Text.Json
serialization. This feature:
- Aligns with modern .NET practices by leveraging the
System.Text.Json
library, which is optimized for .NET. - Provides improved performance and reduced memory usage compared to the legacy
Newtonsoft.Json
-based implementation.
The following benchmarks compare the performance of the new System.Text.Json
implementation with the legacy Newtonsoft.Json
implementation:
Scenario | Implementation | Mean | Allocated Memory |
---|---|---|---|
Application Benchmarks | Newtonsoft.JsonPatch | 271.924 µs | 25 KB |
System.Text.JsonPatch | 1.584 µs | 3 KB | |
Deserialization Benchmarks | Newtonsoft.JsonPatch | 19.261 µs | 43 KB |
System.Text.JsonPatch | 7.917 µs | 7 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 new implementation doesn't support dynamic types, for example,
- The JSON Patch standard has inherent security risks. Since these risks are inherent to the JSON Patch standard, the new implementation doesn'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 the Mitigating Security Risks section.
Usage
To enable JSON Patch support with System.Text.Json
, install the Microsoft.AspNetCore.JsonPatch.SystemTextJson
NuGet package.
dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease
This package provides a JsonPatchDocument#### T>
class to represent a JSON Patch document for objects of type T
and custom logic for serializing and deserializing JSON Patch documents using System.Text.Json
. The key method of the JsonPatchDocument<T>
class is ApplyTo
, which applies the patch operations to a target object of type T
.
The following examples demonstrate how to use the ApplyTo
method to apply a JSON Patch document to an object.
Example: Applying a JsonPatchDocument
The following example demonstrates:
- The
add
,replace
, andremove
operations. - Operations on nested properties.
- Adding a new item to an array.
- Using a JSON String Enum Converter in a JSON patch document.
// Original object
var 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 document
string 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 document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document
patchDoc!.ApplyTo(person);
// Output updated object
Console.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"
// }
// ]
// }
The ApplyTo
method generally follows the conventions and options of System.Text.Json
for processing the JsonPatchDocument
, including the behavior controlled by the following options:
NumberHandling
: Whether numeric properties can be read from strings.PropertyNameCaseInsensitive
: Whether property names are case-sensitive.
Key differences between System.Text.Json
and the new JsonPatchDocument<T>
implementation:
- The runtime type of the target object, not the declared type, determines which properties
ApplyTo
patches. System.Text.Json
deserialization relies on the declared type to identify eligible properties.
Example: Applying a JsonPatchDocument 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 the test
operation. The test
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 the ApplyTo
method is modified in place. It is the caller's responsiblity to discard these changes if any operation fails.
// Original object
var person = new Person {
FirstName = "John",
LastName = "Doe",
Email = "johndoe@gmail.com"
};
// Raw JSON patch document
string 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 document
var patchDoc = JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// Apply the JSON patch document, catching any errors
Dictionary<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 object
Console.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 the Microsoft.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 a
copy
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 calling
ApplyTo
. - The validation needs to be app specific, but an example validation can look similar to the following:
- Validate incoming JSON Patch documents for size and structure before calling
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 needs.
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, (e.g., 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 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:
- It doesn't have the host or authority section.
- It has an absolute path.
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!
Related content
ASP.NET Core