.NET 11 Preview 6 includes new ASP.NET Core features and improvements:
- Minimal API validation runs async validators
- Blazor Virtualize can scroll to an item
- OpenAPI documents default to 3.2 and describe union types
- Short-circuit endpoints with an attribute
- SignalR refreshes authentication on long-lived connections
- Breaking changes
- Bug fixes
- Community contributors
ASP.NET Core updates in .NET 11:
Minimal API validation now supports asynchronous validators end-to-end (dotnet/aspnetcore #66487, dotnet/aspnetcore #67183). Preview 5 shipped the building blocks for asynchronous form validation in Blazor. Preview 6 adds the new asynchronous DataAnnotations APIs in the base libraries — AsyncValidationAttribute, IAsyncValidatableObject, and Validator.ValidateObjectAsync — and Microsoft.Extensions.Validation now runs them when an endpoint validates a request.
This lets a validation rule do real work, such as a database lookup or a remote API call, without blocking a thread. A model implements IAsyncValidatableObject and returns validation results as an IAsyncEnumerable<ValidationResult>. Because IAsyncValidatableObject extends IValidatableObject, also implement the synchronous Validate method (return no results when all the validation is asynchronous):
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
public class ReservationRequest : IAsyncValidatableObject
{
[Required]
public string Email { get; set; } = "";
public DateOnly Date { get; set; }
// IValidatableObject (synchronous) — no synchronous rules here.
public IEnumerable<ValidationResult> Validate(ValidationContext context) => [];
public async IAsyncEnumerable<ValidationResult> ValidateAsync(
ValidationContext context,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var rooms = context.GetRequiredService<IRoomService>();
if (!await rooms.HasAvailabilityAsync(Date, cancellationToken))
{
yield return new ValidationResult(
"No rooms are available on that date.", [nameof(Date)]);
}
}
}Register validation and the framework validates the request before the endpoint runs:
builder.Services.AddValidation();
app.MapPost("/reservations", (ReservationRequest request) =>
Results.Ok(request));Validators run concurrently where possible: asynchronous attributes on the same member start together, collection items validate in parallel, and the framework preserves the existing ordering between member, type, and IValidatableObject validation. For attribute-based rules, derive from AsyncValidationAttribute and override its IsValidAsync method. Thank you @Youssef1313 for the implementation work on this feature.
The Virtualize<TItem> component can now open at a specific item and scroll to any item on demand (dotnet/aspnetcore #66753). Two new public APIs make this possible:
InitialIndexpositions the list at a given item on the first interactive render, so the list opens at that item without a flash of the first item.ScrollToIndexAsync(int itemIndex, CancellationToken cancellationToken = default)scrolls to an item at any time after the first render and returns aTaskthat completes when the target is aligned to the top of the viewport.
<Virtualize TItem="Product" Items="products" InitialIndex="500" @ref="list">
<div class="product">@context.Name</div>
</Virtualize>
<button @onclick="GoToTop">Back to top</button>
@code {
private Virtualize<Product> list = default!;
private List<Product> products = ProductCatalog.All;
private async Task GoToTop() => await list.ScrollToIndexAsync(0);
}Out-of-range indexes are clamped to the valid range. If a second ScrollToIndexAsync call starts while one is still in flight, the last call wins. Calling ScrollToIndexAsync before the first interactive render throws InvalidOperationException; use InitialIndex to set the starting position instead.
Generated OpenAPI documents now target OpenAPI 3.2 by default (dotnet/aspnetcore #67097, dotnet/aspnetcore #67007). Endpoints that return C# union types are now described with an anyOf schema listing each case type (dotnet/aspnetcore #67001). Unlike polymorphic types, union cases don't carry a $type discriminator, so a case schema such as Kitten reuses the standalone #/components/schemas/Kitten component instead of producing a duplicated, prefixed component. ApiExplorer detects a union through JsonTypeInfoKind.Union on the parent type, so the same metadata flows through to MVC controllers, minimal APIs, and external consumers such as Swashbuckle and NSwag.
This builds on the Preview 5 support for declaring multiple response types per status code, so an endpoint that returns one of several shapes is now described accurately in the document.
The new [ShortCircuit] attribute marks an endpoint to run immediately after routing, skipping the rest of the middleware pipeline (dotnet/aspnetcore #67249). This is the attribute form of the existing ShortCircuit() endpoint convention, so it can be applied directly to MVC controllers and actions. Short-circuiting is useful for endpoints that don't need authentication, CORS, or other middleware — for example a health check or a robots.txt response — and it avoids the cost of running that middleware. The endpoint still runs and produces its response; pass an optional status code, such as [ShortCircuit(404)], to set the response status code. Thank you @Porozhniakov for contributing this feature!
[ApiController]
[Route("robots.txt")]
[ShortCircuit]
public class RobotsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Content("User-agent: *\nDisallow:", "text/plain");
}The same attribute works on minimal API endpoints, and the existing ShortCircuit() convention continues to work unchanged:
app.MapGet("/health", [ShortCircuit] () => "Healthy");SignalR connections can now refresh authentication without dropping the connection when the access token expires (dotnet/aspnetcore #67400). The server exposes a /refresh endpoint alongside /negotiate and reports the token lifetime in the negotiate response. The .NET client automatically re-authenticates before the token expires, so a hub connection that previously closed when its bearer token aged out can stay open.
Enable the feature when mapping the hub:
app.MapHub<ChatHub>("/chat", options =>
{
options.EnableAuthenticationRefresh = true;
});A hub can react to a refreshed identity by overriding OnAuthenticationRefreshedAsync, and the server can customize the refresh decision with HttpConnectionDispatcherOptions.OnAuthenticationRefresh:
public class ChatHub : Hub
{
public override Task OnAuthenticationRefreshedAsync()
{
// The connection's User has been updated with the refreshed token.
return Task.CompletedTask;
}
}- Custom minimal API validation resolvers —
Microsoft.Extensions.Validation.IValidatableInfowas split intoIValidatableTypeInfo,IValidatableParameterInfo, andIValidatablePropertyInfo, andValidateContext.ValidationErrorsis now a read-only dictionary that you add to with the newValidateContext.AddValidationErrormethod (dotnet/aspnetcore #67183). Apps that use the built-in[ValidatableType]andAddValidation()are unaffected; only code that implementsIValidatableInfoResolverdirectly needs to update. - Antiforgery and cross-origin CSRF share one verdict —
CsrfProtectionMiddlewareno longer short-circuits a cross-origin request. It records its verdict onIAntiforgeryValidationFeature, and the request is rejected only when a component actually reads the form (dotnet/aspnetcore #67082). Apps that callUseAntiforgery()see no change. This unifies token antiforgery and cross-origin CSRF behind a single, lazily enforced verdict.
- OpenAPI
- Honored
DescriptionAttributeon nullable value types during schema generation (dotnet/aspnetcore #65245). - Fixed duplicate XML documentation IDs for generic properties and references (dotnet/aspnetcore #64404).
- Propagated
JsonSerializerOptionscorrectly during OpenAPI generation (dotnet/aspnetcore #66847).
- Honored
- Caching
- Added the key sub-delimiter between multi-value
Varyheader values in response and output caching (dotnet/aspnetcore #66936).
- Added the key sub-delimiter between multi-value
- Metrics
- Fixed the default value of
System.Diagnostics.Metrics.Meter.IsSupported(dotnet/aspnetcore #66846).
- Fixed the default value of
Thank you contributors! ❤️