Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
This article highlights the most significant changes in ASP.NET Core 9.0 with links to relevant documentation.
MapStaticAssets
routing endpoint conventions is a new feature that optimizes the delivery of static assets in ASP.NET Core apps.
For information on static asset delivery for Blazor apps, see ASP.NET Core Blazor static files.
Following production best practices for serving static assets requires a significant amount of work and technical expertise. Without optimizations like compression, caching, and fingerprints:
Creating performant web apps requires optimizing asset delivery to the browser. Possible optimizations include:
MapStaticAssets is a new feature that optimizes the delivery of static assets in an app. It's designed to work with all UI frameworks, including Blazor, Razor Pages, and MVC. It's typically a drop-in replacement for UseStaticFiles
:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();
app.Run();
MapStaticAssets
operates by combining build and publish-time processes to collect information about all the static resources in an app. This information is then utilized by the runtime library to efficiently serve these files to the browser.
MapStaticAssets
can replace UseStaticFiles
in most situations, however, it's optimized for serving the assets that the app has knowledge of at build and publish time. If the app serves assets from other locations, such as disk or embedded resources, UseStaticFiles
should be used.
MapStaticAssets
provides the following benefits not found with UseStaticFiles
:
gzip
during development and gzip + brotli
during publish.ETags
: The Etags
for each resource are the Base64 encoded string of the SHA-256 hash of the content. This ensures that the browser only redownloads a file if its contents have changed.The following table shows the original and compressed sizes of the CSS and JS files in the default Razor Pages template:
File | Original | Compressed | % Reduction |
---|---|---|---|
bootstrap.min.css | 163 | 17.5 | 89.26% |
jquery.js | 89.6 | 28 | 68.75% |
bootstrap.min.js | 78.5 | 20 | 74.52% |
Total | 331.1 | 65.5 | 80.20% |
The following table shows the original and compressed sizes using the Fluent UI Blazor components library:
File | Original | Compressed | % Reduction |
---|---|---|---|
fluent.js | 384 | 73 | 80.99% |
fluent.css | 94 | 11 | 88.30% |
Total | 478 | 84 | 82.43% |
For a total of 478 KB uncompressed to 84 KB compressed.
The following table shows the original and compressed sizes using the MudBlazor Blazor components library:
File | Original | Compressed | Reduction |
---|---|---|---|
MudBlazor.min.css | 541 | 37.5 | 93.07% |
MudBlazor.min.js | 47.4 | 9.2 | 80.59% |
Total | 588.4 | 46.7 | 92.07% |
Optimization happens automatically when using MapStaticAssets
. When a library is added or updated, for example with new JavaScript or CSS, the assets are optimized as part of the build. Optimization is especially beneficial to mobile environments that can have a lower bandwidth or an unreliable connections.
For more information on the new file delivery features, see the following resources:
MapStaticAssets
has the following advantages over dynamic compression on the server:
Consider the following table comparing MudBlazor compression with IIS dynamic compression and MapStaticAssets
:
IIS gzip | MapStaticAssets |
MapStaticAssets reduction |
---|---|---|
≅ 90 | 37.5 | 59% |
This section describes new features for Blazor.
A new solution template makes it easier to create .NET MAUI native and Blazor web client apps that share the same UI. This template shows how to create client apps that maximize code reuse and target Android, iOS, Mac, Windows, and Web.
Key features of this template include:
To get started, install the .NET 9 SDK and install the .NET MAUI workload, which contains the template:
dotnet workload install maui
Create a solution from the project template in a command shell using the following command:
dotnet new maui-blazor-web
The template is also available in Visual Studio.
Note
Currently, an exception occurs if Blazor rendering modes are defined at the per-page/component level. For more information, see BlazorWebView needs a way to enable overriding ResolveComponentForRenderMode (dotnet/aspnetcore
#51235).
For more information, see Build a .NET MAUI Blazor Hybrid app with a Blazor Web App.
We've introduced a new API designed to simplify the process of querying component states at runtime. This API provides the following capabilities:
For more information, see ASP.NET Core Blazor render modes.
The following enhancements have been made to the default server-side reconnection experience:
When the user navigates back to an app with a disconnected circuit, reconnection is attempted immediately rather than waiting for the duration of the next reconnect interval. This improves the user experience when navigating to an app in a browser tab that has gone to sleep.
When a reconnection attempt reaches the server but the server has already released the circuit, a page refresh occurs automatically. This prevents the user from having to manually refresh the page if it's likely going to result in a successful reconnection.
Reconnect timing uses a computed backoff strategy. By default, the first several reconnection attempts occur in rapid succession without a retry interval before computed delays are introduced between attempts. You can customize the retry interval behavior by specifying a function to compute the retry interval, as the following exponential backoff example demonstrates:
Blazor.start({
circuit: {
reconnectionOptions: {
retryIntervalMilliseconds: (previousAttempts, maxRetries) =>
previousAttempts >= maxRetries ? null : previousAttempts * 1000
},
},
});
The styling of the default reconnect UI has been modernized.
For more information, see ASP.NET Core Blazor SignalR guidance.
New APIs make it easier to add authentication to an existing Blazor Web App. When you create a new Blazor Web App with authentication using Individual Accounts and you enable WebAssembly-based interactivity, the project includes a custom AuthenticationStateProvider in both the server and client projects.
These providers flow the user's authentication state to the browser. Authenticating on the server rather than the client allows the app to access authentication state during prerendering and before the .NET WebAssembly runtime is initialized.
The custom AuthenticationStateProvider implementations use the Persistent Component State service (PersistentComponentState) to serialize the authentication state into HTML comments and read it back from WebAssembly to create a new AuthenticationState instance.
This works well if you've started from the Blazor Web App project template and selected the Individual Accounts option, but it's a lot of code to implement yourself or copy if you're trying to add authentication to an existing project. There are now APIs, which are now part of the Blazor Web App project template, that can be called in the server and client projects to add this functionality:
By default, the API only serializes the server-side name and role claims for access in the browser. An option can be passed to AddAuthenticationStateSerialization to include all claims.
For more information, see the following sections of ASP.NET Core Blazor authentication and authorization:
With the release of .NET 9, it's now simpler to add static SSR pages to apps that adopt global interactivity.
This approach is only useful when the app has specific pages that can't work with interactive Server or WebAssembly rendering. For example, adopt this approach for pages that depend on reading/writing HTTP cookies and can only work in a request/response cycle instead of interactive rendering. For pages that work with interactive rendering, you shouldn't force them to use static SSR rendering, as it's less efficient and less responsive for the end user.
Mark any Razor component page with the new [ExcludeFromInteractiveRouting]
attribute assigned with the @attribute
Razor directive:
@attribute [ExcludeFromInteractiveRouting]
Applying the attribute causes navigation to the page to exit from interactive routing. Inbound navigation is forced to perform a full-page reload instead resolving the page via interactive routing. The full-page reload forces the top-level root component, typically the App
component (App.razor
), to rerender from the server, allowing the app to switch to a different top-level render mode.
The RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting extension method allows the component to detect whether the [ExcludeFromInteractiveRouting]
attribute is applied to the current page.
In the App
component, use the pattern in the following example:
[ExcludeFromInteractiveRouting]
attribute default to the InteractiveServer
render mode with global interactivity. You can replace InteractiveServer
with InteractiveWebAssembly
or InteractiveAuto
to specify a different default global render mode.[ExcludeFromInteractiveRouting]
attribute adopt static SSR (PageRenderMode
is assigned null
).<!DOCTYPE html>
<html>
<head>
...
<HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
<Routes @rendermode="@PageRenderMode" />
...
</body>
</html>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? PageRenderMode
=> HttpContext.AcceptsInteractiveRouting() ? InteractiveServer : null;
}
An alternative to using the RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting extension method is to read endpoint metadata manually using HttpContext.GetEndpoint()?.Metadata
.
This feature is covered by the reference documentation in ASP.NET Core Blazor render modes.
Razor components support constructor injection.
In the following example, the partial (code-behind) class injects the NavigationManager
service using a primary constructor:
public partial class ConstructorInjection(NavigationManager navigation)
{
private void HandleClick()
{
navigation.NavigateTo("/counter");
}
}
For more information, see ASP.NET Core Blazor dependency injection.
By default, Interactive Server components enable compression for WebSocket connections and set a frame-ancestors
Content Security Policy (CSP) directive set to 'self'
, which only permits embedding the app in an <iframe>
of the origin from which the app is served when compression is enabled or when a configuration for the WebSocket context is provided.
Compression can be disabled by setting ConfigureWebSocketOptions
to null
, which reduces the vulnerability of the app to attack but may result in reduced performance:
.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)
Configure a stricter frame-ancestors
CSP with a value of 'none'
(single quotes required), which allows WebSocket compression but prevents browsers from embedding the app into any <iframe>
:
.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy = "'none'")
For more information, see the following resources:
The new KeyboardEventArgs.IsComposing
property indicates if the keyboard event is part of a composition session. Tracking the composition state of keyboard events is crucial for handling international character input methods.
The QuickGrid
component now exposes an OverscanCount
property that specifies how many additional rows are rendered before and after the visible region when virtualization is enabled.
The default OverscanCount
is 3. The following example increases the OverscanCount
to 4:
<QuickGrid ItemsProvider="itemsProvider" Virtualize="true" OverscanCount="4">
...
</QuickGrid>
The InputNumber<TValue> component now supports the type="range"
attribute, which creates a range input that supports model binding and form validation, typically rendered as a slider or dial control rather than a text box:
<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">
<div>
<label>
Nacelle Count (2-6):
<InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2"
step="1" type="range" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>
@code {
[SupplyParameterFromForm]
private EngineSpecifications? Model { get; set; }
protected override void OnInitialized() => Model ??= new();
private void Submit() {}
public class EngineSpecifications
{
[Required, Range(minimum: 2, maximum: 6)]
public int NacelleCount { get; set; }
}
}
Trigger JavaScript callbacks either before or after enhanced navigation with new event listeners:
blazor.addEventListener("enhancednavigationstart", {CALLBACK})
blazor.addEventListener("enhancednavigationend", {CALLBACK})
For more information, see ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR).
Interactive WebAssembly rendering in Blazor now supports client-side request streaming using the request.SetBrowserReqeustStreamingEnabled(true)
option on HttpRequestMessage
.
For more information, see the following resources:
This section describes new features for SignalR.
Hub methods can now accept a base class instead of the derived class to enable polymorphic scenarios. The base type needs to be annotated to allow polymorphism.
public class MyHub : Hub
{
public void Method(JsonPerson person)
{
if (person is JsonPersonExtended)
{
}
else if (person is JsonPersonExtended2)
{
}
else
{
}
}
}
[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
public string Name { get; set; }
public Person Child { get; set; }
public Person Parent { get; set; }
}
private class JsonPersonExtended : JsonPerson
{
public int Age { get; set; }
}
private class JsonPersonExtended2 : JsonPerson
{
public string Location { get; set; }
}
SignalR now has an ActivitySource for both the hub server and client.
The SignalR ActivitySource named Microsoft.AspNetCore.SignalR.Server
emits events for hub method calls:
The following example uses the .NET Aspire dashboard and the OpenTelemetry packages:
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
Add the following startup code to the Program.cs
file:
using OpenTelemetry.Trace;
using SignalRChat.Hubs;
// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where your OTEL endpoint is.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
if (builder.Environment.IsDevelopment())
{
// View all traces only in development environment.
tracing.SetSampler(new AlwaysOnSampler());
}
tracing.AddAspNetCoreInstrumentation();
tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
});
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter());
var app = builder.Build();
The following is example output from the Aspire Dashboard:
The SignalR ActivitySource named Microsoft.AspNetCore.SignalR.Client
emits events for a SignalR client:
ActivitySource
named Microsoft.AspNetCore.SignalR.Client
. Hub invocations now create a client span. Note that other SignalR clients, such as the JavaScript client, don't support tracing. This feature will be added to more clients in future releases.Here's how these new activities look in the .NET Aspire dashboard:
Continuing the Native AOT journey started in .NET 8, we have enabled trimming and native ahead-of-time (AOT) compilation 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.
Install the latest .NET 9 SDK.
Create a solution from the webapiaot
template in a command shell using the following command:
dotnet new webapiaot -o SignalRChatAOTExample
Replace the contents of the Program.cs
file 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);
}
}
The preceding example produces a native Windows executable of 10 MB and a Linux executable of 10.9 MB.
System.Text.Json
Source Generator.IAsyncEnumerable<T>
and ChannelReader<T>
where T
is a ValueType (struct
) aren't supported. Using these types results in a runtime exception at startup in development and in the published app. For more information, see SignalR: Using IAsyncEnumerable<T> and ChannelReader<T> with ValueTypes in native AOT (dotnet/aspnetcore
#56179).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.Task
, Task<T>
, ValueTask
, or ValueTask<T>
are supported for async return types.This section describes new features for minimal APIs.
The TypedResults class is a helpful vehicle for returning strongly-typed HTTP status code-based responses from a minimal API. TypedResults
now includes factory methods and types for returning "500 Internal Server Error" responses from endpoints. Here's an example that returns a 500 response:
var app = WebApplication.Create();
app.MapGet("/", () => TypedResults.InternalServerError("Something went wrong!"));
app.Run();
The ProducesProblem
and ProducesValidationProblem
extension methods have been updated to support their use on route groups. These methods 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 .NET 9, 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?>
. In this release, these construction APIs support overloads that consume IEnumerable<KeyValuePair<string, object?>>
.
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!
This section describes new features for OpenAPI
The OpenAPI specification is a standard for describing HTTP APIs. The standard allows developers to define the shape of APIs that can be plugged into client generators, server generators, testing tools, documentation, and more. In .NET 9, ASP.NET Core provides built-in support for generating OpenAPI documents representing controller-based or minimal APIs via the Microsoft.AspNetCore.OpenApi package.
The following highlighted code calls:
AddOpenApi
to register the required dependencies into the app's DI container.MapOpenApi
to register the required OpenAPI endpoints in the app's routes.var builder = WebApplication.CreateBuilder();
builder.Services.AddOpenApi();
var app = builder.Build();
app.MapOpenApi();
app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);
app.Run();
Install the Microsoft.AspNetCore.OpenApi
package in the project using the following command:
dotnet add package Microsoft.AspNetCore.OpenApi
Run the app and navigate to openapi/v1.json
to view the generated OpenAPI document:
OpenAPI documents can also be generated at build-time by adding the Microsoft.Extensions.ApiDescription.Server
package:
dotnet add package Microsoft.Extensions.ApiDescription.Server
To modify the location of the emitted OpenAPI documents, set the target path in the OpenApiDocumentsDirectory property in the app's project file:
<PropertyGroup>
<OpenApiDocumentsDirectory>$(MSBuildProjectDirectory)</OpenApiDocumentsDirectory>
</PropertyGroup>
Run dotnet build
and inspect the generated JSON file in the project directory.
ASP.NET Core's built-in OpenAPI document generation provides support for various customizations and options. It provides document, operation, and schema transformers and has the ability to manage multiple OpenAPI documents for the same application.
To learn more about ASP.NET Core's new OpenAPI document capabilities, see the new Microsoft.AspNetCore.OpenApi docs.
OpenAPI in ASP.NET Core supports trimming and native AOT. The following steps create and publish an OpenAPI app with 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
Update Program.cs
to enable generating OpenAPI documents.
+ builder.Services.AddOpenApi();
var app = builder.Build();
+ app.MapOpenApi();
Publish the app.
dotnet publish
This section describes new features for authentication and authorization.
We'd like to thank Joe DeCock from Duende Software 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. Thats is, moving authorization parameters from redirect URLs in the browser to direct machine to machine http calls on the back end.
This prevents a cyberattacker in the browser from:
- Seeing authorization parameters, which could leak PII.
- Tampering with those parameters. For example, the cyberattacker could change the scope of access being requested.
Pushing the authorization parameters also keeps request URLs short. Authorize parameters can get very long when using more complex OAuth and OIDC features such as Rich Authorization Requests. URLs that are long cause issues in many browsers and networking infrastructures.
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
For .NET 9, we have decided to enable PAR by default if the identity provider's discovery document advertises support for PAR, since it should provide enhanced security for providers that support it. The identity provider's discovery document is usually found at .well-known/openid-configuration
. 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("oidc", oidcOptions =>
{
// Other provider-specific configuration goes here.
// The default value is PushedAuthorizationBehavior.UseIfAvailable.
// 'OpenIdConnectOptions' does not contain a definition for 'PushedAuthorizationBehavior'
// and no accessible extension method 'PushedAuthorizationBehavior' accepting a first argument
// of type 'OpenIdConnectOptions' could be found
oidcOptions.PushedAuthorizationBehavior = PushedAuthorizationBehavior.Disable;
});
To ensure that authentication only succeeds if PAR is used, 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. See the API proposal for more details.
The OAuth and OIDC authentication handlers now have an AdditionalAuthorizationParameters
option to make it easier to customize authorization message parameters that are usually included as part of the redirect query string. In .NET 8 and earlier, this requires a custom OnRedirectToIdentityProvider callback or overridden BuildChallengeUrl method in a custom handler. Here's an example of .NET 8 code:
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
options.Events.OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("prompt", "login");
context.ProtocolMessage.SetParameter("audience", "https://api.example.com");
return Task.CompletedTask;
};
});
The preceding example can now be simplified to the following code:
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
options.AdditionalAuthorizationParameters.Add("prompt", "login");
options.AdditionalAuthorizationParameters.Add("audience", "https://api.example.com");
});
You can now configure the HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING
and HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL
HTTP.sys flags by using the new EnableKerberosCredentialCaching
and CaptureCredentials
properties on the HTTP.sys AuthenticationManager to optimize how Windows authentication is handled. For example:
webBuilder.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
options.Authentication.EnableKerberosCredentialCaching = true;
options.Authentication.CaptureCredentials = true;
});
The following sections describe miscellaneous new features.
Important
HybridCache
is currently still in preview but will be fully released after .NET 9.0 in a future minor release of .NET Extensions.
The HybridCache
API bridges some gaps in the existing IDistributedCache and IMemoryCache APIs. It also adds new capabilities, such as:
HybridCache
is designed to be a drop-in replacement for existing IDistributedCache
and IMemoryCache
usage, and it provides a simple API for adding new caching code. It provides a unified API for both in-process and out-of-process caching.
To see how the HybridCache
API is simplified, compare it to code that uses IDistributedCache
. Here's an example of what using IDistributedCache
looks like:
public class SomeService(IDistributedCache cache)
{
public async Task<SomeInformation> GetSomeInformationAsync
(string name, int id, CancellationToken token = default)
{
var key = $"someinfo:{name}:{id}"; // Unique key for this combination.
var bytes = await cache.GetAsync(key, token); // Try to get from cache.
SomeInformation info;
if (bytes is null)
{
// Cache miss; get the data from the real source.
info = await SomeExpensiveOperationAsync(name, id, token);
// Serialize and cache it.
bytes = SomeSerializer.Serialize(info);
await cache.SetAsync(key, bytes, token);
}
else
{
// Cache hit; deserialize it.
info = SomeSerializer.Deserialize<SomeInformation>(bytes);
}
return info;
}
// This is the work we're trying to cache.
private async Task<SomeInformation> SomeExpensiveOperationAsync(string name, int id,
CancellationToken token = default)
{ /* ... */ }
}
That's a lot of work to get right each time, including things like serialization. And in the cache miss scenario, you could end up with multiple concurrent threads, all getting a cache miss, all fetching the underlying data, all serializing it, and all sending that data to the cache.
To simplify and improve this code with HybridCache
, we first need to add the new library Microsoft.Extensions.Caching.Hybrid
:
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0" />
Register the HybridCache
service, like you would register an IDistributedCache
implementation:
builder.Services.AddHybridCache(); // Not shown: optional configuration API.
Now most caching concerns can be offloaded to HybridCache
:
public class SomeService(HybridCache cache)
{
public async Task<SomeInformation> GetSomeInformationAsync
(string name, int id, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(
$"someinfo:{name}:{id}", // Unique key for this combination.
async cancel => await SomeExpensiveOperationAsync(name, id, cancel),
token: token
);
}
}
We provide a concrete implementation of the HybridCache
abstract class via dependency injection, but it's intended that developers can provide custom implementations of the API. The HybridCache
implementation deals with everything related to caching, including concurrent operation handling. The cancel
token here represents the combined cancellation of all concurrent callers—not just the cancellation of the caller we can see (that is, token
).
High throughput scenarios can be further optimized by using the TState
pattern, to avoid some overhead from captured variables and per-instance callbacks:
public class SomeService(HybridCache cache)
{
public async Task<SomeInformation> GetSomeInformationAsync(string name, int id, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(
$"someinfo:{name}:{id}", // unique key for this combination
(name, id), // all of the state we need for the final call, if needed
static async (state, token) =>
await SomeExpensiveOperationAsync(state.name, state.id, token),
token: token
);
}
}
HybridCache
uses the configured IDistributedCache
implementation, if any, for secondary out-of-process caching, for example, using
Redis. But even without an IDistributedCache
, the HybridCache
service will still provide in-process caching and "stampede" protection.
In typical existing code that uses IDistributedCache
, every retrieval of an object from the cache results in deserialization. This behavior means that each concurrent caller gets a separate instance of the object, which cannot interact with other instances. The result is thread safety, as there's no risk of concurrent modifications to the same object instance.
Because a lot of HybridCache
usage will be adapted from existing IDistributedCache
code, HybridCache
preserves this behavior by default to avoid introducing concurrency bugs. However, a given use case is inherently thread-safe:
In such cases, inform HybridCache
that it's safe to reuse instances by:
sealed
. The sealed
keyword in C# means that the class can't be inherited.[ImmutableObject(true)]
attribute to it. The [ImmutableObject(true)]
attribute indicates that the object's state can't be changed after it's created.By reusing instances, HybridCache
can reduce the overhead of CPU and object allocations associated with per-call deserialization. This can lead to performance improvements in scenarios where the cached objects are large or accessed frequently.
Like IDistributedCache
, HybridCache
supports removal by key with a RemoveKeyAsync
method.
HybridCache
also provides optional APIs for IDistributedCache
implementations, to avoid byte[]
allocations. This feature is implemented
by the preview versions of the Microsoft.Extensions.Caching.StackExchangeRedis
and Microsoft.Extensions.Caching.SqlServer
packages.
Serialization is configured as part of registering the service, with support for type-specific and generalized serializers via the
WithSerializer
and .WithSerializerFactory
methods, chained from the AddHybridCache
call. By default, the library
handles string
and byte[]
internally, and uses System.Text.Json
for everything else, but you can use protobuf, xml, or anything
else.
HybridCache
supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0.
For more information about HybridCache
, see HybridCache library in ASP.NET Core
The ASP.NET Core developer exception page is displayed when an app throws an unhandled exception during development. The developer exception page provides detailed information about the exception and request.
Preview 3 added endpoint metadata to the developer exception page. ASP.NET Core uses endpoint metadata to control endpoint behavior, such as routing, response caching, rate limiting, OpenAPI generation, and more. The following image shows the new metadata information in the Routing
section of the developer exception page:
While testing the developer exception page, small quality of life improvements were identified. They shipped in Preview 4:
The following animated image shows the new developer exception page:
The debugging display of dictionaries and other key-value collections has an improved layout. The key is displayed in the debugger's key column instead of being concatenated with the value. The following images show the old and new display of a dictionary in the debugger.
Before:
After:
ASP.NET Core has many key-value collections. This improved debugging experience applies to:
By default there is now a 1 second delay between when IIS is notified of a recycle or shutdown and when ANCM tells the managed server to start shutting down. The delay is configurable via the ANCM_shutdownDelay
environment variable or by setting the shutdownDelay
handler setting. Both values are in milliseconds. The delay is mainly to reduce the likelihood of a race where:
Slower machines or machines with heavier CPU usage may want to adjust this value to reduce 503 likelihood.
Example of setting shutdownDelay
:
<aspNetCore processPath="dotnet" arguments="myapp.dll" stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
<handlerSettings>
<!-- Milliseconds to delay shutdown by.
this doesn't mean incoming requests will be delayed by this amount,
but the old app instance will start shutting down after this timeout occurs -->
<handlerSetting name="shutdownDelay" value="5000" />
</handlerSettings>
</aspNetCore>
The fix is in the globally installed ANCM module that comes from the hosting bundle.
It seems intuitive that an [Authorize]
attribute placed "closer" to an MVC action than an [AllowAnonymous]
attribute would override the [AllowAnonymous]
attribute and force authorization. However, this is not necessarily the case. What does matter is the relative order of the attributes.
The following code shows examples where a closer [Authorize]
attribute gets overridden by an [AllowAnonymous]
attribute that is farther away.
[AllowAnonymous]
public class MyController
{
[Authorize] // Overridden by the [AllowAnonymous] attribute on the class
public IActionResult Private() => null;
}
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}
[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}
public class MyControllerInherited2 : MyControllerAnon
{
[Authorize] // Overridden by the [AllowAnonymous] attribute on MyControllerAnon
public IActionResult Private() => null;
}
[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}
In .NET 9 Preview 6, we've introduced an analyzer that will highlight instances like these where a closer [Authorize]
attribute gets overridden by an [AllowAnonymous]
attribute that is farther away from an MVC action. The warning points to the overridden [Authorize]
attribute with the following message:
ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away
The correct action to take if you see this warning depends on the intention behind the attributes. The farther away [AllowAnonymous]
attribute should be removed if it's unintentionally exposing the endpoint to anonymous users. If the [AllowAnonymous]
attribute was intended to override a closer [Authorize]
attribute, you can repeat the [AllowAnonymous]
attribute after the [Authorize]
attribute to clarify the intent.
[AllowAnonymous]
public class MyController
{
// This produces no warning because the second, "closer" [AllowAnonymous]
// clarifies that [Authorize] is intentionally overridden.
// Specifying AuthenticationSchemes can still be useful
// for endpoints that allow but don't require authenticated users.
[Authorize(AuthenticationSchemes = "Cookies")]
[AllowAnonymous]
public IActionResult Privacy() => null;
}
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 among 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:
For more information, see ASP.NET Core metrics.
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);
};
});
A new option when configuring the ExceptionHandlerMiddleware
enables app developers to choose what status code to return when an exception occurs during 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,
});
.NET 9 introduces the ability to opt-out of HTTP metrics for specific endpoints and requests. Opting out of recording metrics is beneficial for endpoints frequently called by automated systems, such as health checks. Recording metrics for these requests is generally unnecessary.
HTTP requests to an endpoint can be excluded from metrics by adding metadata. Either:
[DisableHttpMetrics]
attribute to the Web API controller, SignalR hub or gRPC service.var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHealthChecks();
var app = builder.Build();
app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();
The MetricsDisabled
property has been added to IHttpMetricsTagsFeature
for:
// Middleware that conditionally opts-out HTTP requests.
app.Use(async (context, next) =>
{
var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
if (metricsFeature != null &&
context.Request.Headers.ContainsKey("x-disable-metrics"))
{
metricsFeature.MetricsDisabled = true;
}
await next(context);
});
Prior to .NET 9, data protection keys were not deletable by design, to prevent data loss. Deleting a key renders its protected data irretrievable. Given their small size, the accumulation of these keys generally posed minimal impact. However, to accommodate extremely long-running services, we have introduced the option to delete keys. Generally, only old keys should be deleted. Only delete keys when you can accept the risk of data loss in exchange for storage savings. We recommend data protection keys should not be deleted.
using Microsoft.AspNetCore.DataProtection.KeyManagement;
var services = new ServiceCollection();
services.AddDataProtection();
var serviceProvider = services.BuildServiceProvider();
var keyManager = serviceProvider.GetService<IKeyManager>();
if (keyManager is IDeletableKeyManager deletableKeyManager)
{
var utcNow = DateTimeOffset.UtcNow;
var yearAgo = utcNow.AddYears(-1);
if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate < yearAgo))
{
Console.WriteLine("Failed to delete keys.");
}
else
{
Console.WriteLine("Old keys deleted successfully.");
}
}
else
{
Console.WriteLine("Key manager does not support deletion.");
}
Middleware now supports Keyed DI in both the constructor and the Invoke
/InvokeAsync
method:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");
var app = builder.Build();
app.UseMiddleware<MyMiddleware>();
app.Run();
internal class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next,
[FromKeyedServices("test")] MySingletonClass service)
{
_next = next;
}
public Task Invoke(HttpContext context,
[FromKeyedServices("test2")]
MyScopedClass scopedService) => _next(context);
}
On Ubuntu and Fedora based Linux distros, dotnet dev-certs https --trust
now configures ASP.NET Core HTTPS development certificate as a trusted certificate for:
Previously, --trust
only worked on Windows and macOS. Certificate trust is applied per-user.
To establish trust in OpenSSL, the dev-certs
tool:
~/.aspnet/dev-certs/trust
SSL_CERT_DIR
environment variable.To establish trust in dotnet, the tool puts the certificate in the My/Root
certificate store.
To establish trust in NSS databases, if any, the tool searches the home directory for Firefox profiles, ~/.pki/nssdb
, and ~/snap/chromium/current/.pki/nssdb
. For each directory found, the tool adds an entry to the nssdb
.
The ASP.NET Core project templates and libraries have been updated to use the latest versions of Bootstrap, jQuery, and jQuery Validation, specifically:
ASP.NET Core feedback
ASP.NET Core is an open source project. Select a link to provide feedback:
Events
Power BI DataViz World Championships
Feb 14, 4 PM - Mar 31, 4 PM
With 4 chances to enter, you could win a conference package and make it to the LIVE Grand Finale in Las Vegas
Learn moreTraining
Module
Improve the developer experience of an API with Swagger documentation - Training
Learn how to document an existing API, written in C#/ASP.NET Core, using Swashbuckle, Swagger/OpenAPI, and Swagger UI.