Closed danroth27 closed 1 week ago
(Added to What's New as an include with PR #33318)
Continuing the native AOT journey we started in .NET 8, we have enabled trimming and native AOT support for both SignalR client and server scenarios. You can now take advantage of the performance benefits of using native AOT in applications that use SignalR for real-time web communications.
Using the dotnet new webapiaot
template, and replacing Program.cs
with the following SignalR code:
using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
<title>SignalR Chat</title>
</head>
<body>
<input id="userInput" placeholder="Enter your name" />
<input id="messageInput" placeholder="Type a message" />
<button onclick="sendMessage()">Send</button>
<ul id="messages"></ul>
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.7/signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();
connection.on("ReceiveMessage", (user, message) => {
const li = document.createElement("li");
li.textContent = `${user}: ${message}`;
document.getElementById("messages").appendChild(li);
});
async function sendMessage() {
const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
await connection.invoke("SendMessage", user, message);
}
connection.start().catch(err => console.error(err));
</script>
</body>
</html>
""", "text/html"));
app.Run();
[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
public class ChatHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Produces a native Windows executable of 10 MB
and a Linux executable of 10.9 MB
.
IAsyncEnumerable<T>
and ChannelReader<T>
where T
is a ValueType (i.e. struct
) are not supported
Microsoft.AspNetCore.SignalR.Hub.IsCustomAwaitableSupported
AppContext switch and SignalRCustomAwaitableSupport
MSBuild property) that is disabled by default in trimmed and AOT'd apps@halter73 the only customer-facing change worth mentioning is the new analyzer that you've done (AllowAnonymous / Authorize). Can you please drop a note as a comment here with the details about it? Thanks!
The AllowAnonymous / Authorize changes were covered in preview 6. https://github.com/dotnet/AspNetCore.Docs/issues/32960#issuecomment-2214749440
We'd like to thank @josephdecock from @DuendeSoftware for adding Pushed Authorization Requests (PAR) to ASP.NET Core's OpenIdConnectHandler
. Joe described the background and motivation for enabling PAR in his API proposal as follows:
Pushed Authorization Requests (PAR) is a relatively new OAuth standard that improves the security of OAuth and OIDC flows by moving authorization parameters from the front channel to the back channel (that is, from redirect URLs in the browser to direct machine to machine http calls on the back end).
This prevents an attacker in the browser from
- seeing authorization parameters (which could leak PII) and from
- tampering with those parameters (e.g., the attacker could change the scope of access being requested).
Pushing the authorization parameters also keeps request URLs short. Authorize parameters might get very long when using more complex OAuth and OIDC features such as Rich Authorization Requests, and URLs that are long cause issues in many browsers and networking infrastructure.
The use of PAR is encouraged by the FAPI working group within the OpenID Foundation. For example, the FAPI2.0 Security Profile requires the use of PAR. This security profile is used by many of the groups working on open banking (primarily in Europe), in health care, and in other industries with high security requirements.
PAR is supported by a number of identity providers, including
- Duende IdentityServer
- Curity
- Keycloak
- Authlete
For preview7, we have decided to enable PAR by default if the identity provider's discovery document (usually found at .well-known/openid-configuration
) advertises support for PAR, since it should provide enhanced security for providers that support it. If this causes problems, you can disable PAR via OpenIdConnectOptions.PushedAuthorizationBehavior
as follows:
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(oidcOptions =>
{
// Other provider-specific configuration goes here.
// The default value is PushedAuthorizationBehavior.UseIfAvailable.
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
});
If you want to ensure that authentication only succeeds if PAR is used, you can use PushedAuthorizationBehavior.Require
instead. This change also introduces a new OnPushAuthorization
event to OpenIdConnectEvents
which can be used customize the pushed authorization request or handle it manually. Please refer to the API proposal for more details.
@josephdecock @brockallen @JeremyLikness @blowdart @jennyf19 @janmarques @brentschmaltz Let me know if you think there's any improvements we should make to the above for the preview7 announcement.
ProducesProblem
and ProducesValidationProblem
on route groupsThe ProducesProblem
and ProducesValidationProblem
extension methods have been updated to support application on route groups, as in the sample below. These methods can be used to indicate that all endpoints in a route group can return ProblemDetails
or ValidationProblemDetails
responses for the purposes of OpenAPI metadata.
var app = WebApplication.Create();
var todos = app.MapGroup("/todos")
.ProducesProblem();
todos.MapGet("/", () => new Todo(1, "Create sample app", false));
todos.MapPost("/", (Todo todo) => Results.Ok(todo));
app.Run();
record Todo(int Id, string Title, boolean IsCompleted);
Problem
and ValidationProblem
result types support construction with IEnumerable<KeyValuePair<string, object?>>
valuesPrior to this preview, constructing Problem
and ValidationProblem
result types in minimal APIs required that the errors
and extensions
properties be initialized with an implementation of IDictionary<string, object?>
. Starting in this release, these construction APIs support overloads that consume IEnumerable<KeyValuePair<string, object?>>
.
using Microsoft.AspNetCore.Http;
var app = WebApplication.Create();
app.MapGet("/", () =>
{
var extensions = new List<KeyValuePair<string, object>> { new("test", "value") };
return TypedResults.Problem("This is an error with extensions", extensions: extensions);
});
Thanks to GitHub user "joegoldman2" for this contribution!
The new built-in OpenAPI support in ASP.NET Core now also supports trimming and Native AOT.
Create a new ASP.NET Core Web API (native AOT) project.
dotnet new webapiaot
Add the Microsoft.AspNetCore.OpenAPI package.
dotnet add package Microsoft.AspNetCore.OpenApi --prerelease
For this prerelease, you also need to add the latest Microsoft.OpenAPI package to avoid trimming warnings.
dotnet add package Microsoft.OpenApi
Update Program.cs to enable generating OpenAPI documents.
+ builder.Services.AddOpenApi();
var app = builder.Build();
+ app.MapOpenApi();
Publish the app.
dotnet publish
The app should publish cleanly using Native AOT without warnings.
OpenAPI transformers support modifying the OpenAPI document, operations within the document, or schemas associated with types in the API. In this preview, the APIs for registering transformers on an OpenAPI document to provide a variety of options for registering transformers.
Previously, the following APIs where available for registering transformers:
OpenApiOptions UseTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions UseTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions UseTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions UseSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task>)
OpenApiOptions UseOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task>)
The new API set is as follows:
OpenApiOptions AddDocumentTransformer(Func<OpenApiDocument, OpenApiDocumentTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddDocumentTransformer(IOpenApiDocumentTransformer transformer)
OpenApiOptions AddDocumentTransformer<IOpenApiDocumentTransformer>()
OpenApiOptions AddSchemaTransformer(Func<OpenApiSchema, OpenApiSchemaTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddSchemaTransformer(IOpenApiSchemaTransformer transformer)
OpenApiOptions AddSchemaTransformer<IOpenApiSchemaTransformer>()
OpenApiOptions AddOperationTransformer(Func<OpenApiOperation, OpenApiOperationTransformerContext, CancellationToken, Task> transformer)
OpenApiOptions AddOperationTransformer(IOpenApiOperationTransformer transformer)
OpenApiOptions AddOperationTransformer<IOpenApiOperationTransformer>()
Thanks to GitHub user "martincostello" for contributing this API improvement to the package!
Historically, it has been intentionally impossible to delete data protection keys because doing so makes it impossible to decrypt any data protected with them (i.e. causing data loss). Fortunately, keys are quite small, so the impact of accumulating many of them is minor. However, in order to support very long running services, we've added the ability to explicitly delete (typically, very old) keys, accepting the risk of data loss in exchange for storage savings. Our guidance remains that data protection keys should not be deleted.
[I'd rather not include this code snippet, but here it is:]
var keyManager = services.GetService<IKeyManager>();
if (keyManager is IDeletableKeyManager deletableKeyManager)
{
var utcNow = DateTimeOffset.UtcNow;
var yearGo = utcNow.AddYears(-1);
if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearGo))
{
throw new InvalidOperationException("Failed to delete keys.");
}
}
Kestrel's named pipe support has been improved with advanced customization options. The new CreateNamedPipeServerStream
method on the named pipe options allows pipes to be customized per-endpoint.
An example of where this is useful is a Kestrel app that requires two pipe endpoints but with different security. The CreateNamedPipeServerStream
option can be used to create pipes with different security, depending on the pipe name.
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenNamedPipe("pipe1");
options.ListenNamedPipe("pipe2");
});
builder.WebHost.UseNamedPipes(options =>
{
options.CreateNamedPipeServerStream = (context) =>
{
var pipeSecurity = CreatePipeSecurity(context.NamedPipeEndpoint.PipeName);
return NamedPipeServerStreamAcl.Create(context.NamedPipeEndPoint.PipeName, PipeDirection.InOut,
NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte,
context.PipeOptions, inBufferSize: 0, outBufferSize: 0, pipeSecurity);
};
});
There is a new option when configuring the ExceptionHandlerMiddleware
that allows application developers to choose what status code to return when an exception occurs during application request handling. This also changes the status code being set in the ProblemDetails response from the ExceptionHandlerMiddleware
.
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex is TimeoutException
? StatusCodes.Status503ServiceUnavailable
: StatusCodes.Status500InternalServerError,
});
Thanks to @latonz for contributing this new option!
.NET 9 adds the ability to opt-out of HTTP metrics and not record a value for certain endpoints and requests. It's common for apps to have endpoints that are frequently called by automated systems, such as a health checks endpoint. Recording information about those requests isn't useful.
HTTP requests to an endpoint can be excluded from metrics by adding metadata. Either add the [DisableHttpMetrics]
attribute to your Web API controller, SignalR hub or gRPC service; or call DisableHttpMetrics()
when mapping endpoints in app startup:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();
In more advanced scenarios where a request doesn't map to an endpoint, or you want to opt-out HTTP requests dynamically, the MetricsDisabled
property has been added to IHttpMetricsTagsFeature
. Set MetricsDisabled
to true during a HTTP request to opt-out.
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
if (context.Request.Headers.ContainsKey("x-disable-metrics"))
{
context.Features.Get<IHttpMetricsTagsFeature>()?.MetricsDisabled = true;
}
await next(context);
});
We've made a significant improvement to Kestrel's connection metrics by including metadata about why a connection failed. The kestrel.connection.duration
metric now includes the connection close reason in the error.type
attribute.
Here is a small sample of error.type
values:
tls_handshake_failed
- The connection requires TLS, and the TLS handshake failed.connection_reset
- The connection was unexpectedly closed by the client while requests were in progress.request_headers_timeout
- Kestrel closed the connection because it didn't receive request headers in time.max_request_body_size_exceeded
- Kestrel closed the connection because uploaded data exceeded max size.Previously, diagnosing Kestrel connection issues required a server to record detailed, low-level logging. The problem with logs is they're expensive to generate and store. And it can be difficult to find the right information amongst the noise.
Metrics are a much cheaper alternative that can be left on in a production environment with minimal impact. Collected metrics can drive dashboards and alerts. Once a problem is identified at a high-level with metrics, further investigation using logging and other tooling can begin.
We expect improved connection metrics to be useful in many scenarios:
Learn more about ASP.NET Core metrics here.
@Rick-Anderson and @tdykstra, It is not clear to me if it is OK now to do PR's adding the new sections (I have one ready to submit). Assuming the process would be to do the PR, get the review and merge to main, but ensure it is not merged to live. I don't remember us using a fork or anything last time.
Got my answer offline, will PR now, thanks!
Added PR #33318 SignalR supports trimming and native AOT
Description
Update the "What's new in ASP.NET Core 9.0" for .NET 9 Preview 7 .NET 9 preview releases
Page URL
https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-9.0
Content source URL
https://github.com/dotnet/AspNetCore.Docs/blob/main/aspnetcore/release-notes/aspnetcore-9.0.md
Document ID
4e75ad25-2c3f-b28e-6a91-ac79a9c683b6
Article author
@Rick-Anderson
Associated WorkItem - 291114