-
Notifications
You must be signed in to change notification settings - Fork 24.7k
What's new in ASP.NET Core in .NET 11 Preview 6 #37334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||
| ### Async validation for minimal APIs | ||||||||
|
|
||||||||
| Minimal API validation supports asynchronous validators end-to-end. The base libraries add the asynchronous DataAnnotations APIs `AsyncValidationAttribute`, `IAsyncValidatableObject`, and `Validator.ValidateObjectAsync`, and `Microsoft.Extensions.Validation` runs them when an endpoint validates a request. | ||||||||
|
|
||||||||
| <!-- TODO: Update `AsyncValidationAttribute`, `IAsyncValidatableObject`, and `Validator.ValidateObjectAsync` to <xref:> once API docs are published. --> | ||||||||
|
|
||||||||
| Asynchronous validation 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. When a type validates asynchronously only, throw from `Validate` so it isn't silently validated through the synchronous APIs: | ||||||||
|
|
||||||||
| ```csharp | ||||||||
| 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) — this type validates asynchronously only. | ||||||||
| public IEnumerable<ValidationResult> Validate(ValidationContext context) => | ||||||||
| throw new NotSupportedException("Validate this type with ValidateAsync."); | ||||||||
|
|
||||||||
| public async IAsyncEnumerable<ValidationResult> ValidateAsync( | ||||||||
| ValidationContext context, | ||||||||
| [EnumeratorCancellation] CancellationToken cancellationToken = default) | ||||||||
| { | ||||||||
| var rooms = context.GetRequiredService<IRoomService>(); | ||||||||
| if (!await rooms.HasAvailabilityAsync(Date, cancellationToken)) | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional; but AFAIK (and what I've been doing in the Blazor node and samples), framework coding calls for a line between the start of an
Suggested change
|
||||||||
| { | ||||||||
| 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: | ||||||||
|
|
||||||||
| ```csharp | ||||||||
| 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](https://github.com/Youssef1313) for the implementation work on this feature! | ||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,11 +1,22 @@ | ||||||
| ### C# union types | ||||||
|
|
||||||
| ASP.NET Core supports [C# union types](/dotnet/csharp/whats-new/csharp-14#union-types) (new in .NET 11) anywhere [`System.Text.Json`](/dotnet/standard/serialization/system-text-json/overview) is used: JSON request and response bodies in Minimal APIs and MVC, SignalR's `JsonHubProtocol`, Blazor JavaScript interop, persistent component state, and prerendered component parameters. | ||||||
| C# union types are a preview language feature in .NET 11 (see the [C# release notes](/dotnet/csharp/whats-new/csharp-14#union-types)), and [`System.Text.Json`](/dotnet/standard/serialization/system-text-json/overview) serializes them natively. Because ASP.NET Core uses `System.Text.Json` for JSON, union types work as JSON request bodies and return types throughout the stack with no ASP.NET-specific configuration: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could also place the C# language reference cross-link here as well, but this is What's New, so perhaps it's best to leave it as-is. I went through on the Blazor PR and updated all of the non-What's New references to use the reference doc, so we won't be saddled with pointing devs to a What's New language feature article in reference content when the language reference article exists. Here's the link in case you want to see or use it ... https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/union |
||||||
|
|
||||||
| * Minimal APIs — union body parameters and return types, including `Task<Union>`, `IAsyncEnumerable<Union>`, and `Results<T1, T2>`, in both the runtime and source-generated request delegates. | ||||||
| * MVC and Razor Pages — union types as `[FromBody]` parameters and action or page-handler return types. | ||||||
| * SignalR — union types as hub method parameters, return values, and stream items when using the JSON hub protocol. | ||||||
| * Blazor — union types as component parameters, JavaScript interop arguments and results, and persisted component state. | ||||||
|
|
||||||
| ```csharp | ||||||
| public union UnionIntString(int, string); | ||||||
| // Requires <LangVersion>preview</LangVersion> in the project file. | ||||||
| public record class Dog(string Name); | ||||||
| public record class Cat(int Lives); | ||||||
| public union Pet(Dog, Cat); | ||||||
|
|
||||||
| app.MapGet("/value", () => new UnionIntString(42)); | ||||||
| // The active case is serialized on the way out and described with anyOf in OpenAPI. | ||||||
| app.MapGet("/pets/{id}", Pet (int id) => id == 0 ? new Dog("Rex") : new Cat(9)); | ||||||
| ``` | ||||||
|
|
||||||
| Union types aren't supported for non-body binding sources such as route values, query strings, headers, and form fields. | ||||||
| For OpenAPI, an endpoint that returns a union is described with an `anyOf` schema listing each case type. Unlike polymorphic types, union cases don't carry a `$type` discriminator, so a case such as `Dog` reuses the standalone `#/components/schemas/Dog` component instead of a duplicated, prefixed one. ApiExplorer detects a union through `JsonTypeInfoKind.Union`, so the schema also flows through to Swashbuckle and NSwag. | ||||||
|
|
||||||
| A few limits apply. Only JSON request bodies and responses are supported — binding a union from the query string, route values, headers, or form fields isn't yet available. When multiple cases serialize to the same JSON shape, disambiguate them with a `[JsonUnion]` classifier. SignalR unions require the JSON hub protocol; the MessagePack and Newtonsoft.Json protocols don't support unions. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||
| ### OpenAPI 3.2 by default | ||||||
|
|
||||||
| Generated OpenAPI documents now target OpenAPI 3.2 by default. Documents continue to generate as before; set the document version explicitly if you need to target an earlier version for tooling that hasn't adopted 3.2 yet. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| To target an earlier version, specify it when calling <xref:Microsoft.Extensions.DependencyInjection.OpenApiServiceCollectionExtensions.AddOpenApi%2A>: | ||||||
|
|
||||||
| ```csharp | ||||||
| builder.Services.AddOpenApi(options => | ||||||
| { | ||||||
| options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_1; | ||||||
| }); | ||||||
| ``` | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||
| ### Short-circuit endpoints with an attribute | ||||||
|
|
||||||
| The new `[ShortCircuit]` attribute marks an endpoint to run immediately after routing, skipping the rest of the middleware pipeline. This is the attribute form of the existing `ShortCircuit()` endpoint convention, so it can be applied directly to MVC controllers and actions. | ||||||
|
|
||||||
| <!-- TODO: Update `[ShortCircuit]` to <xref:> once API docs are published. --> | ||||||
|
|
||||||
| 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. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still need to understand if the repo's convention for code-fencing all filenames and paths remains in effect after the "freshness" pass.
Suggested change
|
||||||
|
|
||||||
| ```csharp | ||||||
| [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: | ||||||
|
|
||||||
| ```csharp | ||||||
| app.MapGet("/health", [ShortCircuit] () => "Healthy"); | ||||||
| ``` | ||||||
|
|
||||||
| Thank you [@Porozhniakov](https://github.com/Porozhniakov) for contributing this feature! | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| ### SignalR authentication refresh | ||
|
|
||
| SignalR connections can refresh authentication without dropping the connection when the access token expires. The server exposes a `/refresh` endpoint alongside `/negotiate` and reports the token lifetime in the negotiate response. The .NET client re-authenticates before the token expires, so a hub connection that previously closed when its bearer token aged out can stay open. This feature is implemented for the .NET client; the JavaScript/TypeScript client and Azure SignalR Service support are in progress. | ||
|
|
||
| <!-- TODO: Update `EnableAuthenticationRefresh`, `OnAuthenticationRefresh`, `OnAuthenticationRefreshedAsync`, and `WithAuthenticationRefresh` to <xref:> once API docs are published. --> | ||
|
|
||
| Enable the feature per hub on the server: | ||
|
|
||
| ```csharp | ||
| app.MapHub<ChatHub>("/chat", options => | ||
| { | ||
| options.EnableAuthenticationRefresh = true; | ||
|
|
||
| // Optional: decide whether a given connection may refresh. | ||
| options.OnAuthenticationRefresh = context => ValueTask.FromResult(true); | ||
| }); | ||
| ``` | ||
|
|
||
| A hub can react to a refreshed identity by overriding `OnAuthenticationRefreshedAsync`: | ||
|
|
||
| ```csharp | ||
| public class ChatHub : Hub | ||
| { | ||
| public override Task OnAuthenticationRefreshedAsync() | ||
| { | ||
| // The connection's User has been updated with the refreshed token. | ||
| return Task.CompletedTask; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Automatic refresh is on by default in the .NET client and is configurable with `WithAuthenticationRefresh`: | ||
|
|
||
| ```csharp | ||
| var connection = new HubConnectionBuilder() | ||
| .WithUrl("https://example.com/chat") | ||
| .WithAuthenticationRefresh(options => | ||
| { | ||
| // EnableAutoRefresh is true by default. | ||
| options.RefreshBeforeExpiration = TimeSpan.FromMinutes(1); | ||
| options.OnAuthenticationRefreshed = context => Task.CompletedTask; | ||
| options.OnAuthenticationRefreshFailed = context => Task.CompletedTask; | ||
| }) | ||
| .Build(); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| ### Cancel hub invocations from the client | ||
|
|
||
| The SignalR client can cancel a regular, non-streaming hub method invocation. Previously only streaming invocations could be canceled from the client. Now, when you pass a <xref:System.Threading.CancellationToken> to <xref:Microsoft.AspNetCore.SignalR.Client.HubConnectionExtensions.InvokeAsync%2A> and cancel it, the client sends a cancellation message and the hub method's `CancellationToken` parameter is triggered on the server. | ||
|
|
||
| ```csharp | ||
| // Client — canceling the token cancels the server-side invocation. | ||
| using var cts = new CancellationTokenSource(); | ||
| var work = connection.InvokeAsync("LongRunningWork", cts.Token); | ||
| // ... | ||
| cts.Cancel(); | ||
| ``` | ||
|
|
||
| ```csharp | ||
| // Hub — accept a CancellationToken to observe client cancellation. | ||
| public class WorkHub : Hub | ||
| { | ||
| public async Task LongRunningWork(CancellationToken cancellationToken) | ||
| { | ||
| await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken); | ||
| } | ||
| } | ||
| ``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.