Skip to content

Latest commit

 

History

History
406 lines (293 loc) · 20.3 KB

aspnetcore.md

File metadata and controls

406 lines (293 loc) · 20.3 KB

ASP.NET Core updates in .NET 9 Preview 7

Here's a summary of what's new in ASP.NET Core in this preview release:

ASP.NET Core updates in .NET 9 Preview 7:

.NET 9 Preview 7:

SignalR supports trimming and Native AOT

Continuing the Native AOT journey we started in .NET 8, we've 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 apps that use SignalR for real-time web communications.

Getting started

Use the dotnet new webapiaot template to create a new project and replace the contents of 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);
    }
}

Publishing this app produces a native Windows executable of 10 MB and a Linux executable of 10.9 MB.

Limitations

  • Only the JSON protocol is currently supported
    • As shown in the preceding code, apps that use JSON serialization and Native AOT must use the System.Text.Json Source Generator. This follows the same approach as minimal APIs.
  • On the SignalR server, Hub method parameters of type IAsyncEnumerable<T> and ChannelReader<T> where T is a ValueType (i.e. struct) aren't supported. Using these types results in a runtime exception at startup in development and in the published app. See dotnet/aspnetcore#56179 for more information.
  • Strongly-typed hubs aren't supported with Native AOT (PublishAot). Using strongly-typed hubs with Native AOT will result in warnings during build and publish, and a runtime exception. Using strongly-typed hubs with trimming (PublishedTrimmed) is supported.
  • Only Task, Task<T>, ValueTask, or ValueTask<T> are supported for async return types.

Microsoft.AspNetCore.OpenApi supports trimming and Native AOT

The new built-in OpenAPI support in ASP.NET Core now also supports trimming and Native AOT.

Get started

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.

Improvements to transformer registration APIs in Microsoft.AspNetCore.OpenApi

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 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 @martincostello for this contribution!

Call ProducesProblem and ProducesValidationProblem on route groups

The ProducesProblem and ProducesValidationProblem extension methods have been updated to support application on route groups. 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);

Construct Problem and ValidationProblem result types with IEnumerable<KeyValuePair<string, object?>> values

Prior to this preview, constructing Problem and ValidationProblem result types in minimal APIs required errors and extensions parameters of type IDictionary<string, object?>. 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);
});

Thank you @joegoldman2 for this contribution!

OpenIdConnectHandler support for Pushed Authorization Requests (PAR)

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

PAR is now enabled by default if the identity provider's discovery document advertises support for it. The identity provider's discovery document is usually found at .well-known/openid-configuration. This change should provide enhanced security for providers that support PAR. If this causes problems, 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;
    });

To ensure that authentication only succeeds if PAR is used, use PushedAuthorizationBehavior.Require.

This change also introduces a new OnPushAuthorization event to OpenIdConnectEvents which can be used to customize the pushed authorization request or handle it manually. Refer to the API proposal for more details.

Data Protection support for deleting keys

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. Only delete keys when you can accept the risk of data loss in exchange for storage savings. Our guidance remains that data protection keys shouldn't be deleted.

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.");
    }
}

Customize Kestrel named pipe endpoints

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 with different access security. The CreateNamedPipeServerStream option can be used to create pipes with custom security settings, 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);
    };
});

Improved Kestrel connection metrics

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 the 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. However, logs can be 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:

  • Investigating performance issues caused by short connection lifetimes.
  • Observing ongoing external attacks on Kestrel that impact performance and stability.
  • Recording attempted external attacks on Kestrel that Kestrel's built-in security hardening prevented.

For more information, see ASP.NET Core metrics.

Opt-out of HTTP metrics on certain endpoints and requests

.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.

Endpoint can be excluded from metrics by adding metadata using either of the following approaches:

  • Add the [DisableHttpMetrics] attribute to your Web API controller, SignalR Hub, or gRPC service
  • 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, use the new MetricsDisabled property on 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);
});

ExceptionHandlerMiddleware option to choose the status code based on the exception

A new option when configuring the ExceptionHandlerMiddleware allows app developers to choose what status code to return when an exception occurs during application request handling. The new option 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!

Community contributors

Thank you contributors! ❤️