Dotnet Whats New
Dotnet Whats New
h WHAT'S NEW
.NET 9
.NET MAUI 9
EF Core 9
h WHAT'S NEW
.NET 8
.NET MAUI 8
EF Core 8
WPF (.NET 8)
h WHAT'S NEW
October 2024
September 2024
August 2024
Language updates
h WHAT'S NEW
What's new in C# 12
What's new in F# 9
Contribute to docs
e OVERVIEW
.NET Foundation
p CONCEPT
.NET developers
a DOWNLOAD
s SAMPLE
b GET STARTED
.NET on Q&A
Community
Release notes
h WHAT'S NEW
.NET
Welcome to what's new in the .NET docs for October 2024. This article lists some of the
major changes to docs during this period.
New articles
BinaryReader.GetString() returns \uFFFD on malformed sequences
CET supported by default
EnumConverter validates registered types to be enum
New security analyzers
New TimeSpan.From*() overloads that take integers
Some SVE APIs removed
User info in mailto: URIs is compared
Windows private key lifetime simplified
.NET fundamentals
New articles
Analyze projects with .NET Upgrade Assistant
CA2022: Avoid inexact read with Stream.Read
CA2265: Do not compare Span<T> to null or default
Configuration source generator
Experimental features in .NET 9+
Intrinsic APIs marked RequiresUnreferencedCode
Microsoft.Testing.Platform architecture
Microsoft.Testing.Platform capabilities
Microsoft.Testing.Platform extensibility
Microsoft.Testing.Platform Services
Native interoperability ABI support
Respect nullable annotations
SYSLIB1230: Deriving from a GeneratedComInterface-attributed interface defined
in another assembly is not supported
Upgrade projects with .NET Upgrade Assistant
What is code analysis with .NET Upgrade Assistant?
Extract schema
Updated articles
HttpWebRequest to HttpClient migration guide - Fix build suggestions
Install .NET on macOS - Rewrite install on macOS article
New articles
Use Copilot Conversational Assessment with the Azure Migrate application and
code assessment tool.
.NET Framework
New articles
February 2024 security and quality rollup
January 2024 cumulative update preview
January 2024 security and quality rollup
March 2024 cumulative update preview
October 2024 cumulative update preview
October 2024 security and quality rollup
Community contributors
The following people contributed to the .NET docs during this period. Thank you! Learn
how to contribute by following the links under "Get involved" in the what's new landing
page.
Welcome to what's new in the .NET docs for September 2024. This article lists some of
the major changes to docs during this period.
New articles
Floating point-to-integer conversions are saturating
FromKeyedServicesAttribute no longer injects non-keyed parameter
HttpClientFactory logging redacts header values by default
IMsoComponent support is opt-in
IncrementingPollingCounter initial callback is asynchronous
ZipArchiveEntry names and comments respect UTF8 flag
.NET fundamentals
New articles
Common IHttpClientFactory usage issues
HttpWebRequest to HttpClient Migration Guide
Intrinsic APIs marked RequiresDynamicCode
Security features
Updated articles
How to customize property names and values with System.Text.Json - Add Copilot
use case
How to write .NET objects as JSON (serialize) - Add GitHub Copilot use case to
serialize JSON
MSBuild reference for .NET SDK projects - Add MSBuild test-related properties
What's new in .NET libraries for .NET 9 - What's new for .NET 9 RC 1
C# language
New articles
Resolve errors and warnings that affect overload resolution.
New articles
Frequently asked questions
.NET Framework
Updated articles
Migrating WSE 3.0 Web Services to WCF - SFI: ROPC - another chunk
Community contributors
The following people contributed to the .NET docs during this period. Thank you! Learn
how to contribute by following the links under "Get involved" in the what's new landing
page.
Welcome to what's new in the .NET docs for August 2024. This article lists some of the
major changes to docs during this period.
New articles
.NET 9 container images no longer install zlib
BigInteger maximum length
Complex.ToString format changed to <a; b>
Deprecated desktop Windows/macOS/Linux MonoVM runtime packages
HostBuilder enables ValidateOnBuild/ValidateScopes in development environment
In-box BinaryFormatter implementation removed and always throws
SafeEvpPKeyHandle.DuplicateHandle up-refs the handle
Some X509Certificate2 and X509Certificate constructors are obsolete
.NET fundamentals
New articles
.NET Runtime metrics
.NET SDK workload sets
.NET uninstall tool overview
BinaryFormatter compatibility package
BinaryFormatter functionality reference
BinaryFormatter migration guide
Choose a serializer
dotnet workload config
dotnet-core-uninstall dry-run
dotnet-core-uninstall list
dotnet-core-uninstall remove
Dynamic adaptation to application sizes (DATAS)
Example: Use OpenTelemetry with Azure Monitor and Application Insights
Example: Use OpenTelemetry with OTLP and the standalone Aspire Dashboard
Example: Use OpenTelemetry with Prometheus, Grafana, and Jaeger
IL2071: 'target generic parameter' generic argument does not satisfy
'DynamicallyAccessedMembersAttribute' in 'target method or type'. The parameter
'source parameter' of method 'source method' does not have matching
annotations. The source value must declare at least the same requirements as
those declared on the target location it is assigned to
IL2076: 'target generic parameter' generic argument does not satisfy
'DynamicallyAccessedMembersAttribute' in 'target method or type'. The return
value of method 'source method' does not have matching annotations. The source
value must declare at least the same requirements as those declared on the target
location it is assigned to
IL2081: 'target generic parameter' generic argument does not satisfy
'DynamicallyAccessedMembersAttribute' in 'target method or type'. The field
'source field' does not have matching annotations. The source value must declare
at least the same requirements as those declared on the target location it is
assigned to
IL2122: Type 'type' is not assembly qualified. Type name strings used for
dynamically accessing a type should be assembly qualified
Microsoft.Testing.Platform configuration settings
Migrate to DataContractSerializer (XML)
Migrate to MessagePack (binary)
Migrate to protobuf-net (binary)
Migrate to System.Text.Json (JSON)
MSTest suppression rules
MSTEST0018: DynamicData should be valid
MSTEST0027: Non-nullable reference not initialized suppressor
MSTEST0028: Non-nullable reference not initialized suppressor
MSTEST0033: Non-nullable reference not initialized suppressor
MSTEST0034: Use ClassCleanupBehavior.EndOfClass with the [ClassCleanup].
MSTEST0035: [DeploymentItem] can be specified only on test class or test method.
MSTEST0036: Do not use shadowing inside test class.
Output extensions
Read BinaryFormatter (NRBF) payloads
SYSLIB0056: Assembly.LoadFrom that takes an AssemblyHashAlgorithm is obsolete
SYSLIB0057: X509Certificate2 and X509Certificate constructors for binary and file
content are obsolete
Windows Forms and Windows Presentation Foundation BinaryFormatter OLE
guidance
Windows Forms migration guide for BinaryFormatter
Windows Presentation Foundation(WPF) migration guide for BinaryFormatter
Updated articles
What's new in .NET libraries for .NET 9 - Update What's new in .NET 9 for Preview 7
What's new in the .NET 9 runtime - Update What's new in .NET 9 for Preview 7
C# language
New articles
Errors and warnings related to partial type and partial member declarations
Partial member (C# Reference)
New articles
Credential chains in the Azure Identity library for .NET
How to customize analysis with run config
Updated articles
Additional methods to authenticate to Azure resources from .NET apps - Add
Interactive brokered authentication and wam content
ML.NET
Updated articles
Tutorial: Automated visual inspection using transfer learning with the ML.NET
Image Classification API - Update image classification tutorial
.NET Framework
New articles
August 2024 security and quality rollup
July 2024 cumulative update preview
Updated articles
.NET Framework data providers - SFI - ROPC: Clean up adonet files
<system.serviceModel> of workflow - SFI - ROPC: First group of updates
Caching support for WCF web HTTP services - SFI: ROPC - Fix up WCF docs
Code access security and ADO.NET - SFI - ROPC: Clean up adonet files
Code generation in LINQ to SQL - SFI - ROPC: Clean up adonet/sql files
Configuring Discovery in a Configuration File - Code fence entire element (WCF
docs)
Configuring Services Using Configuration Files - Code fence entire element (WCF
docs)
Connection string syntax - SFI - ROPC: Clean up adonet files
Connection strings and configuration files - SFI - ROPC: Clean up adonet files
Create a DataTable from a DataView - SFI - ROPC: Clean up adonet files
Data retrieval and CUD operations in n-tier applications (LINQ to SQL) - SFI -
ROPC: Clean up adonet/sql files
Date and time data - SFI - ROPC: Clean up adonet/sql files
Enable multiple active result sets - SFI - ROPC: Clean up adonet/sql files
GetSchema and Schema Collections - SFI - ROPC: Clean up adonet files
How to: Create a Federated Client - Code fence entire element (WCF docs)
How to: Deserialize instance data properties - SFI - ROPC: First group of updates
How to: Use Transport Security and Message Credentials - Code fence entire
element (WCF docs)
Insert an image from a file - SFI - ROPC: Clean up adonet/sql files
Integrating with COM+ Applications Overview - Code fence entire element (WCF
docs)
Large UDTs - SFI - ROPC: Clean up adonet/sql files
Manipulate data - SFI - ROPC: Clean up adonet/sql files
Message Security with a Certificate Client - Code fence entire element (WCF docs)
Message Security with Mutual Certificates - SFI: ROPC - Fix up WCF docs
Obtaining a DbProviderFactory - SFI - ROPC: Clean up adonet files
Oracle Sequences - SFI - ROPC: Clean up adonet files
OracleTypes - SFI - ROPC: Clean up adonet files
Partial Trust Feature Compatibility - Code fence entire element (WCF docs)
Pause and Resume a workflow - SFI - ROPC: First group of updates
Perform batch operations using DataAdapters - SFI - ROPC: Clean up adonet files
Polling in console applications - SFI - ROPC: Clean up adonet/sql files
Provider statistics for SQL Server - SFI - ROPC: Clean up adonet/sql files
Required arguments and overload groups - SFI - ROPC: First group of updates
Schema restrictions - SFI - ROPC: Clean up adonet files
Security Behaviors in WCF - SFI: ROPC - Fix up WCF docs
SecurityBindingElement Authentication Modes - Code fence entire element (WCF
docs)
Specify XML values as parameters - SFI - ROPC: Clean up adonet/sql files
Specifying a Custom Crypto Algorithm - Code fence entire element (WCF docs)
SQL Server Connection Pooling (ADO.NET) - SFI - ROPC: Clean up adonet files
SQL Server Express user instances - SFI - ROPC: Clean up adonet/sql files
SqlClient streaming support - SFI - ROPC: Clean up adonet files
SqlClient Support for high availability and disaster recovery - SFI - ROPC: Clean up
adonet/sql files
Standard Endpoints - Code fence entire element (WCF docs)
Synchronous and Asynchronous Operations - Code fence entire element (WCF
docs)
System.Transactions integration with SQL Server - SFI - ROPC: Clean up adonet files
Tracking Events Reference - SFI - ROPC: First group of updates
Tracking Participants - SFI - ROPC: First group of updates
Using the Message Class - Code fence entire element (WCF docs)
Windows applications using callbacks - SFI - ROPC: Clean up adonet/sql files
Community contributors
The following people contributed to the .NET docs during this period. Thank you! Learn
how to contribute by following the links under "Get involved" in the what's new landing
page.
Learn about the new features in .NET 9 and find links to further documentation.
.NET 9, the successor to .NET 8, has a special focus on cloud-native apps and
performance. It will be supported for 18 months as a standard-term support (STS)
release. You can download .NET 9 here .
New for .NET 9, the engineering team posts .NET 9 preview updates on GitHub
Discussions . That's a great place to ask questions and provide feedback about the
release.
.NET runtime
The .NET 9 runtime includes a new attribute model for feature switches with trimming
support. The new attributes make it possible to define feature switches that libraries
can use to toggle areas of functionality.
Garbage collection includes a dynamic adaptation to application size feature that's used
by default instead of Server GC.
.NET libraries
System.Text.Json adds support for nullable reference type annotations and exporting
JSON schemas from types. It adds new options that let you customize the indentation of
written JSON and read multiple root-level JSON values from a single stream.
In LINQ, the new methods CountBy and AggregateBy make it possible to aggregate
state by key without needing to allocate intermediate groupings via GroupBy.
For reflection, the new PersistedAssemblyBuilder type lets you save an emitted assembly.
This new class also includes PDB support, meaning you can emit symbol info and use it
to debug a generated assembly.
The TimeSpan class includes new From* methods that let you create a TimeSpan object
from an int (instead of a double ). These methods help to avoid errors caused by
inherent imprecision in floating-point calculations.
.NET SDK
The .NET 9 SDK introduces workload sets, where all of your workloads stay at a single,
specific version until explicitly updated. For tools, a new option for dotnet tool install
lets users (instead of tool authors) decide whether a tool is allowed to run on a newer
.NET runtime version than the version the tool targets. In addition:
Unit testing has better MSBuild integration that allows you to run tests in parallel.
NuGet security audits run on both direct and transitive package references, by
default.
The terminal logger is enabled by default and also has improved usability. For
example, the total count of failures and warnings is now summarized at the end of
a build.
New MSBuild script analyzers ("build checks") are available.
The SDK can detect and adjust for version mismatches between the .NET SDK and
MSBuild.
The dotnet workload history command shows you the history of workload
installations and modifications for the current .NET SDK installation.
For more information, see What's new in the SDK for .NET 9.
ML.NET
ML.NET is an open-source, cross-platform framework that enables integration of custom
machine-learning models into .NET applications. The latest version, ML.NET 4.0, adds
additional tokenizer support for tokenizers such as Tiktoken and models such as Llama
and CodeGen.
.NET Aspire
.NET Aspire is an opinionated, cloud-ready stack for building observable, production
ready, distributed applications..NET Aspire is delivered through a collection of NuGet
packages that handle specific cloud-native concerns, and is available in preview for .NET
9. For more information, see .NET Aspire.
ASP.NET Core
ASP.NET Core includes improvements to Blazor, SignalR, minimal APIs, OpenAPI, and
authentication and authorization. For more information, see What's new in ASP.NET
Core 9.0.
.NET MAUI
The focus of .NET Multi-platform App UI (.NET MAUI) in .NET 9 is to improve product
quality. For more information about that and new features, see What's new in .NET
MAUI for .NET 9.
EF Core
Entity Framework Core includes significant updates to the database provider for Azure
Cosmos DB for NoSQL. It also includes some steps towards AOT compilation and pre-
compiled queries, among other improvements. For more information, see What's New in
EF Core 9.
C# 13
C# 13 ships with the .NET 9 SDK and includes the following new features:
params collections
F# 9
F# 9 ships with the .NET 9 SDK and includes the following new features:
See also
Our vision for .NET 9 blog post
What's new in ASP.NET Core 9.0
What's new in .NET MAUI
What's new in EF Core
What's new in ASP.NET Core 9.0
Article • 11/05/2024
This article highlights the most significant changes in ASP.NET Core 9.0 with links to
relevant documentation.
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:
Serve a given asset once until the file changes or the browser clears its cache. Set
the ETag header.
Prevent the browser from using old or stale assets after an app is updated. Set the
Last-Modified header.
Set up proper caching headers .
Use caching middleware.
Serve compressed versions of the assets when possible.
Use a CDN to serve the assets closer to the user.
Minimize the size of assets served to the browser. This optimization doesn't
include minification.
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:
diff
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();
app.Run();
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.
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.
All assets are compressed with the goal of reducing the size of the assets to the
minimum.
Content based 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:
ノ Expand table
The following table shows the original and compressed sizes using the Fluent UI Blazor
components library :
ノ Expand table
fluent.css 94 11 88.30%
The following table shows the original and compressed sizes using the MudBlazor
Blazor components library:
ノ Expand table
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 :
ノ Expand table
≅ 90 37.5 59%
Blazor
This section describes new features for Blazor.
The ability to choose a Blazor interactive render mode for the web app.
Automatic creation of the appropriate projects, including a Blazor Web App (global
Interactive Auto rendering) and a .NET MAUI Blazor Hybrid app.
The created projects use a shared Razor class library (RCL) to maintain the UI's
Razor components.
Sample code is included that demonstrates how to use dependency injection to
provide different interface implementations for the Blazor Hybrid app and the
Blazor Web App.
To get started, install the .NET 9 SDK and install the .NET MAUI workload, which
contains the template:
.NET CLI
Create a solution from the project template in a command shell using the following
command:
.NET CLI
7 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.
Determine the current execution location of the component: This can be useful
for debugging and optimizing component performance.
Check if the component is running in an interactive environment: This can be
helpful for components that have different behaviors based on the interactivity of
their environment.
Retrieve the assigned render mode for the component: Understanding the render
mode can help in optimizing the rendering process and improving the overall
performance of a component.
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:
JavaScript
Blazor.start({
circuit: {
reconnectionOptions: {
retryIntervalMilliseconds: (previousAttempts, maxRetries) =>
previousAttempts >= maxRetries ? null : previousAttempts * 1000
},
},
});
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 Secure ASP.NET Core server-side
Blazor apps:
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:
razor
@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.
razor
<!DOCTYPE html>
<html>
<head>
...
<HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
<Routes @rendermode="@PageRenderMode" />
...
</body>
</html>
@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;
This feature is covered by the reference documentation in ASP.NET Core Blazor render
modes.
Constructor injection
Razor components support constructor injection.
In the following example, the partial (code-behind) class injects the NavigationManager
service using a primary constructor:
C#
the app is served when compression is enabled or when a configuration for the
WebSocket context is provided.
C#
.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)
C#
razor
razor
@code {
[SupplyParameterFromForm]
private EngineSpecifications? Model { get; set; }
SignalR
This section describes new features for SignalR.
C#
[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; }
}
Every method is its own activity, so anything that emits an activity during the hub
method call is under the hub method activity.
Hub method activities don't have a parent. This means they are not bundled under
the long-running SignalR connection.
The following example uses the .NET Aspire dashboard and the OpenTelemetry
packages:
XML
<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" />
C#
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
if (builder.Environment.IsDevelopment())
{
// We want to view all traces in development
tracing.SetSampler(new AlwaysOnSampler());
}
tracing.AddAspNetCoreInstrumentation();
tracing.AddSource("Microsoft.AspNetCore.SignalR.Server");
});
builder.Services.ConfigureOpenTelemetryTracerProvider(tracing =>
tracing.AddOtlpExporter());
Getting started
Create a solution from the webapiaot template in a command shell using the following
command:
.NET CLI
Replace the contents of the Program.cs file with the following SignalR code:
C#
using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;
builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});
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();
app.Run();
[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
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 ( struct ) aren't supported. Using these
Minimal APIs
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:
C#
app.Run();
C#
var app = WebApplication.Create();
app.Run();
C#
app.MapGet("/", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test",
"value") };
return TypedResults.Problem("This is an error with extensions",
extensions:
extensions);
});
OpenAPI
This section describes new features for OpenAPI
C#
builder.Services.AddOpenApi();
app.MapOpenApi();
app.Run();
.NET CLI
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:
.NET CLI
XML
<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 .
Console
Console
For this preview, you also need to add the latest Microsoft.OpenAPI package to avoid
trimming warnings.
Console
diff
+ builder.Services.AddOpenApi();
+ app.MapOpenApi();
Console
dotnet publish
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.
Duende IdentityServer
Curity
Keycloak
Authlete
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:
C#
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("oidc", oidcOptions =>
{
// Other provider-specific configuration goes here.
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:
C#
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;
};
});
C#
builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
options.AdditionalAuthorizationParameters.Add("prompt", "login");
options.AdditionalAuthorizationParameters.Add("audience",
"https://api.example.com");
});
C#
webBuilder.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.Negotiate;
options.Authentication.EnableKerberosCredentialCaching = true;
options.Authentication.CaptureCredentials = true;
});
Miscellaneous
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
The HybridCache API bridges some gaps in the existing IDistributedCache and
IMemoryCache APIs. It also adds new capabilities, such as:
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:
C#
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 :
XML
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid"
Version="9.0.0" />
C#
C#
High throughput scenarios can be further optimized by using the TState pattern, to
avoid some overhead from captured variables and per-instance callbacks:
C#
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 such cases, inform HybridCache that it's safe to reuse instances by:
Marking the type as sealed . The sealed keyword in C# means that the class can't
be inherited.
Applying the [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.
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
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:
Better text wrapping. Long cookies, query string values, and method names no
longer add horizontal browser scroll bars.
Bigger text which is found in modern designs.
More consistent table sizes.
The following animated image shows the new developer exception page:
Dictionary debugging improvements
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:
HTTP headers
Query strings
Forms
Cookies
View data
Route data
Features
Slower machines or machines with heavier CPU usage may want to adjust this value to
reduce 503 likelihood.
XML
The fix is in the globally installed ANCM module that comes from the hosting bundle.
The following code shows examples where a closer [Authorize] attribute gets
overridden by an [AllowAnonymous] attribute that is farther away.
C#
[AllowAnonymous]
public class MyController
{
[Authorize] // Overridden by the [AllowAnonymous] attribute on the class
public IActionResult Private() => null;
}
C#
[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}
[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:
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.
C#
[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;
}
failed.
connection_reset - The connection was unexpectedly closed by the client while
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.
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.
C#
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);
};
});
C#
app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex is TimeoutException
? StatusCodes.Status503ServiceUnavailable
: StatusCodes.Status500InternalServerError,
});
HTTP requests to an endpoint can be excluded from metrics by adding metadata. Either:
Add the [DisableHttpMetrics] attribute to the Web API controller, SignalR hub or
gRPC service.
Call DisableHttpMetrics when mapping endpoints in app startup:
C#
C#
await next(context);
});
C#
using Microsoft.AspNetCore.DataProtection.KeyManagement;
C#
Chromium browsers, for example, Google Chrome, Microsoft Edge, and Chromium.
Mozilla Firefox and Mozilla derived browsers.
.NET APIs, for example, HttpClient
Previously, --trust only worked on Windows and macOS. Certificate trust is applied
per-user.
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 focus of .NET Multi-platform App UI (.NET MAUI) in .NET 9 is to improve product
quality. This includes expanding test coverage, end to end scenario testing, and bug
fixing. For more information about the product quality improvements in .NET MAUI 9,
see the following release notes:
) Important
Due to working with external dependencies, such as Xcode or Android SDK Tools,
the .NET MAUI support policy differs from the .NET and .NET Core support
policy . For more information, see .NET MAUI support policy .
Compatibility with Xcode 16, which includes SDK support for iOS 18, iPadOS 18, tvOS 18,
and macOS 15, is required when building with .NET MAUI 9. Xcode 16 requires a Mac
running macOS 14.5 or later.
In .NET 9, .NET MAUI ships as a .NET workload and multiple NuGet packages. The
advantage of this approach is that it enables you to easily pin your projects to specific
versions, while also enabling you to easily preview unreleased or experimental builds.
When you create a new .NET MAUI project the required NuGet packages are
automatically added to the project.
HybridWebView
HybridWebView enables hosting arbitrary HTML/JS/CSS content in a web view, and
enables communication between the code in the web view (JavaScript) and the code
that hosts the web view (C#/.NET). For example, if you have an existing React JS app,
you could host it in a cross-platform .NET MAUI native app, and build the back-end of
the app using C# and .NET.
The web content of the app, which consists of static HTML, JavaScript, CSS, images,
and other files.
A HybridWebView control as part of the app's UI. This can be achieved by
referencing it in the app's XAML.
Code in the web content, and in C#/.NET, that uses the HybridWebView APIs to
send messages between the two components.
The entire app, including the web content, is packaged and runs locally on a device, and
can be published to applicable app stores. The web content is hosted within a native
web view control and runs within the context of the app. Any part of the app can access
external web services, but isn't required to.
A TitleBar can be set as the value of the Window.TitleBar property on any TitleBar:
XAML
<Window.TitleBar>
<TitleBar x:Name="TeamsTitleBar"
Title="Hello World"
Icon="appicon.png"
HeightRequest="46">
<TitleBar.Content>
<SearchBar Placeholder="Search"
PlaceholderColor="White"
MaximumWidthRequest="300"
HorizontalOptions="Fill"
VerticalOptions="Center" />
</TitleBar.Content>
</TitleBar>
</Window.TitleBar>
C#
XAML
7 Note
Mac Catalyst support for the TitleBar control will be added in a future release.
Control enhancements
.NET MAUI 9 includes control enhancements.
XAML
<ContentPage ...>
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding BackCommand}"
IsVisible="{Binding IsBackButtonVisible}"
IconOverride="back.png" />
</Shell.BackButtonBehavior>
...
</ContentPage>
BlazorWebView
On iOS and Mac Catalyst 18, .NET MAUI 9 changes the default behavior for hosting
content in a BlazorWebView to localhost . The internal 0.0.0.1 address used to host
content no longer works and results in the BlazorWebView not loading any content and
rendering as an empty rectangle.
To opt into using the 0.0.0.1 address, add the following code to the CreateMauiApp
method in MauiProgram.cs:
C#
// Set this switch to use the LEGACY behavior of always using 0.0.0.1 to
host BlazorWebView
AppContext.SetSwitch("BlazorWebView.AppHostAddressAlways0000", true);
C#
AppContext.SetSwitch("BlazorWebView.AndroidFireAndForgetAsync", true);
This switch enables BlazorWebView to fire and forget the async disposal that occurs, and
as a result fixes the majority of the disposal deadlocks that occur on Android. For more
information, see Fix disposal deadlocks on Android.
Buttons on iOS
Button controls on iOS now respect spacing, padding, border width, and margins more
accurately than in previous releases. A large image in a Button will now be resized to the
maximum size, taking into account the spacing, padding, border width, and margins.
However, if a Button contains text and an image it might not be possible to fit all the
content inside the button, and so you should size your image manually to achieve your
desired layout.
To opt into using these handlers, add the following code to your MauiProgram class:
C#
ContentPage
In .NET MAUI 9, the HideSoftInputOnTapped property is also supported on Mac Catalyst,
as well and Android and iOS.
XAML
Text alignment
The TextAlignment enumeration adds a Justify member that can be used to align text
in text controls. For example, you can horizontally align text in a Label with
HorizontalTextAlignment.Justify :
XAML
TimePicker
TimePicker gains a TimeSelected event, which is raised when the selected time changes.
The TimeChangedEventArgs object that accompanies the TimeSelected event has
NewTime and OldTime properties, which specify the new and old time, respectively.
WebView
WebView adds a ProcessTerminated event that's raised when a WebView process ends
unexpectedly. The WebViewProcessTerminatedEventArgs object that accompanies this
event defines platform-specific properties that indicate why the process failed.
C#
// in .NET 8
MyLabel.SetBinding(Label.TextProperty, "Text");
// in .NET 9
MyLabel.SetBinding(Label.TextProperty, static (Entry entry) => entry.Text);
Not all methods can be used to define a compiled binding. The expression must be a
simple property access expression. The following examples show valid and invalid
binding expressions:
C#
In addition, .NET MAUI 9 adds a BindingBase.Create method that sets the binding
directly on the object with a Func , and returns the binding object instance:
C#
// in .NET 8
myEntry.SetBinding(Entry.TextProperty, new MultiBinding
{
Bindings = new Collection<BindingBase>
{
new Binding(nameof(Entry.FontFamily), source:
RelativeBindingSource.Self),
new Binding(nameof(Entry.FontSize), source:
RelativeBindingSource.Self),
new Binding(nameof(Entry.FontAttributes), source:
RelativeBindingSource.Self),
},
Converter = new StringConcatenationConverter()
});
// in .NET 9
myEntry.SetBinding(Entry.TextProperty, new MultiBinding
{
Bindings = new Collection<BindingBase>
{
Binding.Create(static (Entry entry) => entry.FontFamily, source:
RelativeBindingSource.Self),
Binding.Create(static (Entry entry) => entry.FontSize, source:
RelativeBindingSource.Self),
Binding.Create(static (Entry entry) => entry.FontAttributes, source:
RelativeBindingSource.Self),
},
Converter = new StringConcatenationConverter()
});
) Important
Compiled bindings are required instead of string-based bindings in NativeAOT
apps, and in apps with full trimming enabled.
By default, .NET MAUI 9 produces build warnings for bindings that don't use compiled
bindings. For more information about XAML compiled bindings warnings, see XAML
compiled bindings warnings.
Dependency injection
In a Shell app, you no longer need to register your pages with the dependency injection
container unless you want to influence the lifetime of the page relative to the container
with the AddSingleton, AddTransient, or AddScoped methods. For more information
about these methods, see Dependency lifetime.
Handler disconnection
When implementing a custom control using handlers, every platform handler
implementation is required to implement the DisconnectHandler() method, to perform
any native view cleanup such as unsubscribing from events. However, prior to .NET
MAUI 9, the DisconnectHandler() implementation is intentionally not invoked by .NET
MAUI. Instead, you'd have to invoke it yourself when choosing to cleanup a control,
such as when navigating backwards in an app.
In .NET MAUI 9, handlers automatically disconnect from their controls when possible,
such as when navigating backwards in an app. In some scenarios you might not want
this behavior. Therefore, .NET MAUI 9 adds a HandlerProperties.DisconnectPolicy
attached property for controlling when handlers are disconnected from their controls.
This property requires a HandlerDisconnectPolicy argument, with the enumeration
defining the following values:
XAML
<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />
C#
C#
video.DisconnectHandlers();
When disconnecting, the DisconnectHandlers method will propagate down the control
tree until it completes or arrives at a control that has set a manual policy.
Multi-window support
.NET MAUI 9 adds the ability to bring a specific window to the front on Mac Catalyst and
Windows with the Application.Current.ActivateWindow method:
C#
Application.Current?.ActivateWindow(windowToActivate);
Native AOT deployment
In .NET MAUI 9 you can opt into Native AOT deployment on iOS and Mac Catalyst.
Native AOT deployment produces a .NET MAUI app that's been ahead-of-time (AOT)
compiled to native code. This produces the following benefits:
For more information, see Native AOT deployment on iOS and Mac Catalyst.
Native embedding
.NET MAUI 9 includes full APIs for native embedding scenarios, which previously had to
be manually added to your project:
C#
#if ANDROID
var mauiContext = new MauiContext(mauiApp.Services, window);
#else
var mauiContext = new MauiContext(mauiApp.Services);
#endif
Alternatively, you can use the ToPlatformEmbedded method, passing in the Window for the
platform on which the app is running:
C#
C#
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiEmbeddedApp<App>();
return builder.Build();
}
}
Project templates
.NET MAUI 9 adds a .NET MAUI Blazor Hybrid and Web App project template to Visual
Studio that creates a solution with a .NET MAUI Blazor Hybrid app with a Blazor Web
app, which share common code in a Razor class library project.
.NET CLI
Resource dictionaries
In .NET MAUI 9, a stand-alone XAML ResourceDictionary (which isn't backed by a code-
behind file) defaults to having its XAML compiled. To opt out of this behavior, specify <?
xaml-comp compile="false" ?> after the XML header.
Trimming
Full trimming is now supported by setting the $(TrimMode) MSBuild property to full .
For more information, see Trim a .NET MAUI app.
Trimming incompatibilities
The following .NET MAUI features are incompatible with full trimming and will be
removed by the trimmer:
Binding expressions where that binding path is set to a string. Instead, use
compiled bindings. For more information, see Compiled bindings.
Implicit conversion operators, when assigning a value of an incompatible type to a
property in XAML, or when two properties of different types use a data binding.
Instead, you should define a TypeConverter for your type and attach it to the type
using the TypeConverterAttribute. For more information, see Define a
TypeConverter to replace an implicit conversion operator.
Loading XAML at runtime with the LoadFromXaml extension method. This XAML
can be made trim safe by annotating all types that could be loaded at runtime with
the DynamicallyAccessedMembers attribute or the DynamicDependency attribute.
However, this is very error prone and isn't recommended.
Receiving navigation data using the QueryPropertyAttribute. Instead, you should
implement the IQueryAttributable interface on types that need to accept query
parameters. For more information, see Process navigation data using a single
method.
The SearchHandler.DisplayMemberName property. Instead, you should provide an
ItemTemplate to define the appearance of SearchHandler results. For more
information, see Define search results item appearance.
ノ Expand table
ported .
ed .
The easiest way to consume a feature switch is by putting the corresponding MSBuild
property into your app's project file (*.csproj), which causes the related code to be
trimmed from the .NET MAUI assemblies.
prefix.
For information about annotating markup extensions with these attributes, see Service
providers.
Xcode sync
.NET MAUI 9 includes Xcode sync ( xcsync ), which is a tool that enables you to use
Xcode for managing Apple specific files with .NET projects, including asset catalogs, plist
files, storyboards, and xib files. The tool has two main commands to generate a
temporary Xcode project from a .NET project, and to synchronize changes from the
Xcode files back to your .NET project.
You use dotnet build with the xcsync-generate or xcsync-sync commands, to generate
or sync these files, and pass in a project file and additional arguments:
.NET CLI
Deprecated APIs
.NET MAUI 9 deprecates some APIs, which will be completely removed in a future
release.
Frame
The Frame control is marked as obsolete in .NET MAUI 9, and will be completely
removed in a future release. The Border control should be used in its place. For more
information see Border.
MainPage
Instead of defining the first page of your app using the MainPage property on an
Application object, you should set the Page property on a Window to the first page of
your app. This is what happens internally in .NET MAUI when you set the MainPage
property, so there's no behavior change introduced by the MainPage property being
marked as obsolete.
The following example shows setting the Page property on a Window, via the
CreateWindow override:
C#
Code that accesses the Application.Current.MainPage property should now access the
Application.Current.Windows[0].Page property for apps with a single window. For apps
which the Page property can be accessed ( Window.Page ). Platform code can retrieve the
app's IWindow object with the Microsoft.Maui.Platform.GetWindow extension method.
While the MainPage property is retained in .NET MAUI 9 it will be completely removed
in a future release.
Compatibility layouts
The compatibility layout classes in the Microsoft.Maui.Controls.Compatibility namespace
have been obsoleted.
VisualElement.OnMeasure
VisualElement.Measure(Double, Double, MeasureFlags)
These are legacy measure methods that don't function correctly with .NET MAUI layout
expectations.
XML
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-
tizen</TargetFrameworks>
<TargetFrameworks
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0
-windows10.0.19041.0</TargetFrameworks>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst;net9.0-
tizen</TargetFrameworks>
<TargetFrameworks
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0
-windows10.0.19041.0</TargetFrameworks>
XML
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
== 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
== 'maccatalyst'">15.0</SupportedOSPlatformVersion>
When debugging and deploying a new .NET MAUI project to Windows, the default
behavior in .NET 9 is to deploy an unpackaged app. To adopt this behavior, see Convert
a packaged .NET MAUI Windows app to unpackaged.
Prior to building your upgraded app for the first time, delete the bin and obj folders.
Any build errors and warnings will guide you towards next steps.
Asset packs
.NET for Android in .NET 9 introduces the ability to place assets into a separate package,
known as an asset pack. This enables you to upload games and apps that would
normally be larger than the basic package size allowed by Google Play. By putting these
assets into a separate package you gain the ability to upload a package which is up to
2Gb in size, rather than the basic package size of 200Mb.
) Important
Asset packs can only contain assets. In the case of .NET for Android this means
items that have the AndroidAsset build action.
.NET MAUI apps define assets via the MauiAsset build action. An asset pack can be
specified via the AssetPack attribute:
XML
<MauiAsset
Include="Resources\Raw\**"
LogicalName="%(RecursiveDir)%(Filename)%(Extension)"
AssetPack="myassetpack" />
7 Note
If you have specific items you want to place in an asset pack you can use the Update
attribute to define the AssetPack metadata:
XML
Asset packs can have different delivery options, which control when your assets will
install on the device:
Install time packs are installed at the same time as the app. This pack type can be
up to 1Gb in size, but you can only have one of them. This delivery type is specified
with InstallTime metadata.
Fast follow packs will install at some point shortly after the app has finished
installing. The app will be able to start while this type of pack is being installed so
you should check it has finished installing before trying to use the assets. This kind
of asset pack can be up to 512Mb in size. This delivery type is specified with
FastFollow metadata.
On demand packs will never be downloaded to the device unless the app
specifically requests it. The total size of all your asset packs can't exceed 2Gb, and
you can have up to 50 separate asset packs. This delivery type is specified with
OnDemand metadata.
In .NET MAUI apps, the delivery type can be specified with the DeliveryType attribute on
a MauiAsset :
XML
For more information about Android asset packs, see Android asset packs.
Android 15 support
.NET for Android in .NET 9 adds .NET bindings for Android 15 (API 35). To build for these
APIs, update the target framework of your project to net9.0-android :
XML
<TargetFramework>net9.0-android</TargetFramework>
7 Note
You can also specify net9.0-android35 as a target framework, but the number 35
will probably change in future .NET releases to match newer Android OS releases.
android-arm
android-x86
This should improve build times and reduce the size of Android .apk files. Note that
Google Play supports splitting up app bundles per architecture.
If you need to build for these architectures, you can add them to your project file
(.csproj):
XML
<RuntimeIdentifiers>android-arm;android-arm64;android-x86;android-
x64</RuntimeIdentifiers>
Or in a multi-targeted project:
XML
<RuntimeIdentifiers
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
== 'android'">android-arm;android-arm64;android-x86;android-
x64</RuntimeIdentifiers>
Android marshal methods can be enabled in your project file (.csproj) via the
$(AndroidEnableMarshalMethods) property:
XML
<PropertyGroup>
<AndroidEnableMarshalMethods>true</AndroidEnableMarshalMethods>
</PropertyGroup>
For specific details about the feature, see the feature documentation or
implementation on GitHub.
Trimming enhancements
In .NET 9, the Android API assemblies (Mono.Android.dll, Java.Interop.dll) are now fully
trim-compatible. To opt into full trimming, set the $(TrimMode) property in your project
file (.csproj):
XML
<PropertyGroup>
<TrimMode>Full</TrimMode>
</PropertyGroup>
This also enables trimming analyzers, so that warnings are introduced for any
problematic C# code.
iOS: 18.0
tvOS: 18.0
Mac Catalyst: 18.0
macOS: 15.0
For more information about .NET 9 on iOS, tvOS, Mac Catalyst, and macOS, see the
following release notes:
Bindings
.NET for iOS 9 introduces the ability to multi-target versions of .NET for iOS bindings.
For example, a library project may need to build for two distinct iOS versions:
XML
<TargetFrameworks>net9.0-ios17.0;net9.0-ios17.2</TargetFrameworks>
This will produce two libraries, one using iOS 17.0 bindings, and one using iOS 17.2
bindings.
) Important
Trimming enhancements
In .NET 9, the iOS and Mac Catalyst assemblies (Microsoft.iOS.dll,
Microsoft.MacCatalyst.dll etc.) are now fully trim-compatible. To opt into full trimming,
set the $(TrimMode) property in your project file (.csproj):
XML
<PropertyGroup>
<TrimMode>Full</TrimMode>
</PropertyGroup>
This also enables trimming analyzers, so that warnings are introduced for any
problematic C# code.
) Important
Your app and it's dependencies must be fully trimmable in order to utilize this
feature.
See also
What's new in .NET 9.
Our Vision for .NET 9
What's New in EF Core 9
Article • 10/21/2024
EF Core 9 (EF9) is the next release after EF Core 8 and is scheduled for release in
November 2024.
EF9 is available as daily builds which contain all the latest EF9 features and API tweaks.
The samples here make use of these daily builds.
Tip
You can run and debug into the samples by downloading the sample code from
GitHub . Each section below links to the source code specific to that section.
EF9 targets .NET 8, and can therefore be used with either .NET 8 (LTS) or a .NET 9
preview .
Tip
The What's New docs are updated for each preview. All the samples are set up to
use the EF9 daily builds , which usually have several additional weeks of
completed work compared to the latest preview. We strongly encourage use of the
daily builds when testing new features so that you're not doing your testing against
stale bits.
2 Warning
In EF 9.0, the Azure Cosmos DB provider is significantly better at identifying partition key
comparisons in your LINQ queries, and extracting them out to ensure your queries are
only sent to the relevant partition; this can greatly improve the performance of your
queries and reduce RU charges. For example:
C#
Console
Note that the WHERE clause does not contain PartitionKey : that comparison has been
"lifted" out and is used to execute the query only against the relevant partition. In
previous versions, the comparison was left in the WHERE clause in many situations,
causing the query to be executed against all partitions and resulting in increased costs
and reduced performance.
In addition, if your query also provides a value for the document's ID property, and
doesn't include any other query operations, the provider can apply an additional
optimization:
C#
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
Console
Here, no SQL query is sent at all. Instead, the provider performs an an extremely efficient
point read ( ReadItem API), which directly fetches the document given the partition key
and ID. This is the most efficient and cost-effective kind of read you can perform in
Azure Cosmos DB; see the Azure Cosmos DB documentation for more information
about point reads.
To learn more about querying with partition keys and point reads, see the querying
documentation page.
Tip
Azure Cosmos DB originally supported a single partition key, but has since expanded
partitioning capabilities to also support subpartitioning through the specification of up
to three levels of hierarchy in the partition key. EF Core 9 brings full support for
hierarchical partition keys, allowing you take advantage of the better performance and
cost savings associated with this feature.
Partition keys are specified using the model building API, typically in
DbContext.OnModelCreating. There must be a mapped property in the entity type for
each level of the partition key. For example, consider a UserSession entity type:
C#
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
The following code specifies a three-level partition key using the TenantId , UserId , and
SessionId properties:
C#
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Tip
This partition key definition follows the example given in Choose your hierarchical
partition keys from the Azure Cosmos DB documentation.
Notice how, starting with EF Core 9, properties of any mapped type can be used in the
partition key. For bool and numeric types, like the int SessionId property, the value is
used directly in the partition key. Other types, like the Guid UserId property, are
automatically converted to strings.
When querying, EF automatically extracts the partition key values from queries and
applies them to the Azure Cosmos DB query API to ensure the queries are constrained
appropriately to the fewest number of partitions possible. For example, consider the
following LINQ query that supplies all three partition key values in the hierarchy:
C#
When executing this query, EF Core will extract the values of the tenantId , userId , and
sessionId parameters, and pass them to the Azure Cosmos DB query API as the
partition key value. For example, see the logs from executing the query above:
Output
Notice that the partition key comparisons have been removed from the WHERE clause,
and are instead used as the partition key for efficient execution:
["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0] .
For more information, see the documentation on querying with partition keys.
Full support for EF's primitive collections, allowing you to perform LINQ querying
on collections of e.g. ints or strings. See What's new in EF8: primitive collections for
more information.
Support for arbitrary querying over non-primitive collections.
Lots of additional LINQ operators are now supported: indexing into collections,
Length / Count , ElementAt , Contains , and many others.
First, previous versions of EF inserted the discriminator value into the JSON id property,
producing documents such as the following:
JSON
{
"id": "Blog|1099",
...
}
This was done in order to allow for documents of different types (e.g. Blog and Post) and
the same key value (1099) to exist within the same container partition. Starting with EF
9.0, the id property contains contains only the key value:
JSON
{
"id": 1099,
...
}
This is a more natural way to map to JSON, and makes it easier for external tools and
systems to interact with EF-generated JSON documents; such external systems aren't
generally aware of the EF discriminator values, which are by default derived from .NET
types.
Note this is a breaking change, since EF will no longer be able to query existing
documents with the old id format. An API has been introduced to revert to the previous
behavior, see the breaking change note and the the documentation for more details.
JSON
{
"id": 1099,
"$type": "Blog",
...
}
This follows the emerging standard for JSON polymorphism, allowing better
interoperability with other tools. For example, .NET's System.Text.Json also supports
polymorphism, using $type as its default discriminator property name (docs).
Note this is a breaking change, since EF will no longer be able to query existing
documents with the old discriminator property name. See the breaking change note for
details on how to revert to the previous naming.
Once your Azure Cosmos DB container is properly set up, using vector search via EF is a
simple matter of adding a vector property and configuring it:
c#
c#
Pagination support
The Azure Cosmos DB provider now allows for paginating through query results via
continuation tokens, which is far more efficient and cost-effective than the traditional use
of Skip and Take :
c#
c#
c#
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Role-based access
Azure Cosmos DB for NoSQL includes a built-in role-based access control (RBAC)
system. This is now supported by EF9 for all data plane operations. However, Azure
Cosmos DB SDK does not support RBAC for management plane operations in Azure
Cosmos DB. Use Azure Management API instead of EnsureCreatedAsync with RBAC.
Synchronous I/O can still be used for now by configuring the warning level
appropriately. For example, in OnConfiguring on your DbContext type:
C#
Note, however, that we plan to fully remove sync support in EF 11, so start updating to
use async methods like ToListAsync and SaveChangesAsync as soon as possible!
2 Warning
NativeAOT and query precompilation are highly experimental features, and are not
yet suited for production use. The support described below should be viewed as
infrastructure towards the final feature, which will likely be released with EF 10. We
encourage you to experiment with the current support and report on your
experiences, but recommend against deploying EF NativeAOT applications in
production.
EF 9.0 brings initial, experimental support for .NET NativeAOT, allowing the publishing of
ahead-of-time compiled applications which make use of EF to access databases. To
support LINQ queries in NativeAOT mode, EF relies on query precompilation: this
mechanism statically identifies EF LINQ queries and generates C# interceptors, which
contain code to execute each specific query. This can significantly cut down on your
application's startup time, as the heavy lifting of processing and compiling your LINQ
queries into SQL no longer happens every time your application starts up. Instead, each
query's interceptor contains the finalized SQL for that query, as well as optimized code
to materialize database results as .NET objects.
c#
EF will generate a C# interceptor into your project, which will take over the query
execution. Instead of processing the query and translating it to SQL every time the
program starts, the interceptor has the SQL embedded right into it (for SQL Server in
this case), allowing your program to start up much faster:
c#
In addition, the same interceptor contains code to materialize your .NET object from
database results:
c#
This uses another new .NET feature - unsafe accessors, to inject data from the database
into your object's private fields.
If you're interested in NativeAOT and like to experiment with cutting-edge features, give
this a try! Just be aware that the feature should be considered unstable, and currently
has many limitations; we expect to stabilize it and make it more suitable for production
usage in EF 10.
The number of improvements is too great to list them all here. Below, some of the more
important improvements are highlighted; see this issue for a more complete listing of
the work done in 9.0.
We'd like to call out Andrea Canciani (@ranma42 ) for his numerous, high-quality
contributions to optimizing the SQL that gets generated by EF Core!
GroupBy
Tip
C#
EF translates this as grouping by each member of the complex type, which aligns with
the semantics of complex types as value objects. For example, on Azure SQL:
SQL
ExecuteUpdate
Tip
Similarly, in EF9 ExecuteUpdate has also been improved to accept complex type
properties. However, each member of the complex type must be specified explicitly. For
example:
C#
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress,
newAddress));
This generates SQL that updates each column mapped to the complex type:
SQL
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
Previously, you had to manually list out the different properties of the complex type in
your ExecuteUpdate call.
Table pruning
As a first example, the SQL generated by EF sometimes contained JOINs to tables which
weren't actually needed in the query. Consider the following model, which uses table-
per-type (TPT) inheritance mapping:
C#
If we then execute the following query to get all Customers with at least one Order:
C#
SQL
Note that the query contained a join to the DiscountedOrders table even though no
columns were referenced on it. EF9 generates a pruned SQL without the join:
c#
C#
SQL
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Note that the [o].[Id] projection isn't needed in the subquery, since the outer SELECT
expression simply counts the rows. EF9 generates the following instead:
SQL
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... and the projection is empty. This may not seem like much, but it can significantly
simplify the SQL in some cases; you're welcome to scroll through some of the SQL
changes in the tests to see the effect.
Tip
) Important
The GREATEST and LEAST functions were introduced to SQL Server/Azure SQL
databases in the 2022 version . Visual Studio 2022 installs SQL Server 2019 by
default. We recommend installing SQL Server Developer Edition 2022 to try out
these new translations in EF9.
For example, queries using Math.Max or Math.Min are now translated for Azure SQL
using GREATEST and LEAST respectively. For example:
C#
This query is translated to the following SQL when using EF9 executing against SQL
Server 2022:
SQL
Math.Min and Math.Max can also be used on the values of a primitive collection. For
example:
C#
SQL
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
C#
This query is translated to the following SQL when using EF9 executing against SQL
Server 2022:
SQL
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
Tip
Except in some special cases, EF Core parameterizes variables used in a LINQ query, but
includes constants in the generated SQL. For example, consider the following query
method:
C#
This translates to the following SQL and parameters when using Azure SQL:
Output
Notice that EF created a constant in the SQL for ".NET Blog" because this value will not
change from query to query. Using a constant allows this value to be examined by the
database engine when creating a query plan, potentially resulting in a more efficient
query.
On the other hand, the value of id is parameterized, since the same query may be
executed with many different values for id . Creating a constant in this case would result
in pollution of the query cache with lots of queries that differ only in id values. This is
very bad for overall performance of the database.
Generally speaking, these defaults should not be changed. However, EF Core 8.0.2
introduces an EF.Constant method which forces EF to use a constant even if a
parameter would be used by default. For example:
C#
Output
C#
The translation now contains a parameter for the ".NET Blog" string:
Output
EF8 changed the way some queries that use primitive collections are translated. When a
LINQ query contains a parameterized primitive collection, EF converts its contents to
JSON and pass it as a single parameter value the query:
C#
This allows having the same SQL query for different parameterized collections (only the
parameter value changes), but in some situations it can lead to performance issues as
the database isn't able to optimally plan for the query. The EF.Constant method can be
used to revert to the previous translation.
C#
SQL
Tip
The EF.Parameter method overrides the context option. If you want to prevent
parameterization of primitive collections for most of your queries (but not all), you
can set the context option TranslateParameterizedCollectionsToConstants and use
EF.Parameter for the queries or individual variables that you want to parameterize.
Tip
C#
In EF8, the query for dotnetPosts is executed as one round trip, and then the final
results are executed as second query. For example, on SQL Server:
SQL
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SQL
C#
First, Select computes LatestPostRating for each Post which requires a subquery when
translating to SQL. Later in the query these results are aggregated using Average
operation. The resulting SQL looks as follows when run on SQL Server:
SQL
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
In previous versions EF Core would generate invalid SQL for similar queries, trying to
apply the aggregate operation directly over the subquery. This is not allowed on SQL
Server and results in an exception. Same principle applies to queries using aggregate
over another aggregate:
C#
7 Note
This change doesn't affect Sqlite, which supports aggregates over subqueries (or
other aggregates) and it does not support LATERAL JOIN ( APPLY ). Below is the SQL
for the first query running on Sqlite:
SQL
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
Tip
In EF8, the following LINQ query was translated to use the SQL COUNT function:
C#
SQL
C#
SQL
which filters out entities whose NullableIntOne or NullableIntTwo are set to null.
In EF9 we produce:
SQL
C#
SQL
which returns false for entities whose NullableIntOne or NullableIntTwo are set to null
(rather than true expected in C#). Running the same scenario on Sqlite generated:
SQL
which results in Nullable object must have a value exception, as translation produces
null value for cases where NullableIntOne or NullableIntTwo are null.
EF9 now properly handles these scenarios, producing results consistent with LINQ to
Objects and across different providers.
This enhancement was contributed by @ranma42 . Many thanks!
an argument. Instead, they apply default ordering - for entities this means ordering
based on primary key values and for other types, ordering based on the values
themselves.
Below is an example query which takes advantage of the simplified ordering operators:
C#
C#
SQL
7 Note
Order and OrderDescending methods are only supported for collections of entities,
complex types or scalars - they will not work on more complex projections, e.g.
collections of anonymous types containing multiple properties.
This enhancement was contributed by the EF Team alumnus @bricelam . Many thanks!
C#
SQL
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
SQL
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Another example, applicable to SQL Server, is a negated conditional operation.
C#
SQL
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
SQL
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
C#
EF8 would generate a CASE block because comparisons can't appear in the projection
directly in SQL Server queries:
SQL
In EF9, this translation has been simplified and now uses bitwise NOT ( ~ ):
SQL
(#33678 ).
ToString over enums is now translated (#33706 , contributed by
@Danevandy99 ).
string.Join now translates to CONCAT_WS in non-aggregate context on SQL
Server (#28899 ).
EF.Functions.PatIndex now translates to the SQL Server PATINDEX function, which
The above were only some of the more important query improvements in EF9; see this
issue for a more complete listing.
Migrations
7 Note
If you are using Sqlite database, see potential issues associated with this feature.
of EnsureCreatedAsync ).
7 Note
If the application had ran previously, the database may already contain the sample
data (which would have been added on the first initialization of the context). As
such, UseSeeding UseAsyncSeeding should check if data exists before attempting to
populate the database. This can be achieved by issuing a simple EF query.
C#
Model building
Auto-compiled models
Tip
Compiled models can improve startup time for applications with large models--that is
entity type counts in the 100s or 1000s. In previous versions of EF Core, a compiled
model had to be generated manually, using the command line. For example:
.NET CLI
Starting with EF9, this .UseModel line is no longer needed when the application's
DbContext type is in the same project/assembly as the compiled model. Instead, the
compiled model will be detected and used automatically. This can be seen by having EF
log whenever it is building the model. Running a simple application then shows EF
building the model when the application starts:
Output
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
The output from running dotnet ef dbcontext optimize on the model project is:
Output
PS
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\Model> dotnet ef dbcontext optimize
Notice that the log output indicates that the model was built when running the
command. If we now run the application again, after rebuilding but without making any
code changes, then the output is:
Output
Starting application...
Model loaded with 2 entity types.
Notice that the model was not built when starting the application because the compiled
model was detected and used automatically.
MSBuild integration
With the above approach, the compiled model still needs to be regenerated manually
when the entity types or DbContext configuration is changed. However, EF9 ships with
MSBuild and targets package that can automatically update the compiled model when
the model project is built! To get started, install the
Microsoft.EntityFrameworkCore.Tasks NuGet package. For example:
.NET CLI
Use the package version in the command above that matches the version of EF
Core that you are using.
Then enable the integration by setting the EFOptimizeContext property to your .csproj
file. For example:
XML
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>
There are additional, optional, MSBuild properties for controlling how the model is built,
equivalent to the options passed on the command line to dotnet ef dbcontext
optimize . These include:
ノ Expand table
DbContextName The DbContext class to use. Class name only or fully qualified with
namespaces. If this option is omitted, EF Core will find the context class. If
there are multiple context classes, this option is required.
EFStartupProject Relative path to the startup project. Default value is the current folder.
EFTargetNamespace The namespace to use for all generated classes. Defaults to generated from
the root namespace and the output directory plus CompiledModels.
XML
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>
Now, if we build the project, we can see logging at build time indicating that the
compiled model is being built:
Output
Optimizing DbContext...
dotnet exec --depsfile
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual
Studio\Shared\NuGetPackages"
--runtimeconfig
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\App\bin\Release\net8.0\App.runtimeconfig.json
G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-
preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext
optimize --output-dir
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\Model\bin\Release\net8.0\Model.dll --startup-assembly
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\App\bin\Release\net8.0\App.dll
--project-dir
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\App
--verbose
--no-color
--prefix-output
And running the application shows that the compiled model has been detected and
hence the model is not built again:
Output
Starting application...
Model loaded with 2 entity types.
Now, whenever the model changes, the compiled model will be automatically rebuilt as
soon as the project is built.
7 Note
We are working through some performance issues with changes made to the
compiled model in EF8 and EF9. See Issue 33483# for more information.
Tip
EF8 introduced support for mapping arrays and mutable lists of primitive types. This has
been expanded in EF9 to include read-only collections/lists. Specifically, EF9 supports
collections typed as IReadOnlyList , IReadOnlyCollection , or ReadOnlyCollection . For
example, in the following code, DaysVisited will be mapped by convention as a
primitive collection of dates:
C#
The read-only collection can be backed by a normal, mutable collection if desired. For
example, in the following code, DaysVisited can be mapped as a primitive collection of
dates, while still allowing code in the class to manipulate the underlying list.
C#
These collections can then be used in queries in the normal way. For example, this LINQ
query:
C#
SQL
Tip
EF9 supports specification of the SQL Server fill-factor when using EF Core Migrations to
create keys and indexes. From the SQL Server docs, "When an index is created or rebuilt,
the fill-factor value determines the percentage of space on each leaf-level page to be
filled with data, reserving the remainder on each page as free space for future growth."
The fill-factor can be set on a single or composite primary and alternate keys and
indexes. For example:
C#
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
When applied to existing tables, this will alter the tables to the fill-factor to the
constraint:
SQL
Tip
Public model building conventions for applications were introduced in EF7. In EF9, we
have made it easier to extend some of the existing conventions. For example, the code
to map properties by attribute in EF7 is this:
C#
public class AttributeBasedPropertyDiscoveryConvention :
PropertyDiscoveryConvention
{
public
AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDepend
encies dependencies)
: base(dependencies)
{
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
C#
public class
AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDepend
encies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out
CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType,
out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute),
inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
C#
As an aside, some people think this pattern is an abomination because it couples the
entity type to the configuration. Other people think it is very useful because it co-locates
configuration with the entity type. Let's not debate this here. :-)
Tip
C#
C#
If daisy has a HierarchyId of /4/1/3/1/ , then, child1 will get the HierarchyId
"/4/1/3/1/1/", and child2 will get the HierarchyId "/4/1/3/1/2/".
To create a node between these two children, an additional sub-level can be used. For
example:
C#
Tooling
Fewer rebuilds
The dotnet ef command line tool by default builds your project before executing the
tool. This is because not rebuilding before running the tool is a common source of
confusion when things don't work. Experienced developers can use the --no-build
option to avoid this build, which may be slow. However, even the --no-build option
could cause the project to be re-built the next time it is built outside of the EF tooling.
.NET 8 is the successor to .NET 7. It will be supported for three years as a long-term
support (LTS) release. You can download .NET 8 here .
.NET runtime
The .NET 8 runtime includes improvements to performance, garbage collection, and the
core and extension libraries. It also includes a new globalization mode for mobile apps
and new source generators for COM interop and configuration binding. For more
information, see What's new in the .NET 8 runtime.
.NET SDK
For information about what's new in the .NET SDK, code analysis, and diagnostics, see
What's new in the SDK and tooling for .NET 8.
C# 12
C# 12 shipped with the .NET 8 SDK. For more information, see What's new in C# 12.
.NET Aspire
.NET Aspire is an opinionated, cloud-ready stack for building observable, production
ready, distributed applications..NET Aspire is delivered through a collection of NuGet
packages that handle specific cloud-native concerns, and is available in preview for .NET
8. For more information, see .NET Aspire.
ASP.NET Core
ASP.NET Core includes improvements to Blazor, SignalR, minimal APIs, Native AOT,
Kestrel and HTTP.sys servers, and authentication and authorization. For more
information, see What's new in ASP.NET Core 8.0.
.NET MAUI
.NET MAUI includes new functionality for controls, gesture recognizers, Windows apps,
navigation, and platform integration. It also includes some behavior changes and many
performance enhancements. For more information, see What's new in .NET MAUI for
.NET 8.
EF Core
Entity Framework Core includes improvements to complex type objects, collections of
primitive types, JSON column mapping, raw SQL queries, lazy loading, tracked-entity
access, model building, math translations, and other features. It also includes a new
HierarchyId type. For more information, see What's New in EF Core 8.
Windows Forms
Windows Forms includes improvements to data binding, Visual Studio DPI, and high
DPI. Button commands are also fully enabled now. For more information, see What's
new for .NET 8 (Windows Forms).
See also
Breaking changes in .NET 8
This article highlights the most significant changes in ASP.NET Core 8.0 with links to
relevant documentation.
Blazor
Full-stack web UI
With the release of .NET 8, Blazor is a full-stack web UI framework for developing apps
that render content at either the component or page level with:
Static Server rendering (also called static server-side rendering, static SSR) to
generate static HTML on the server.
Interactive Server rendering (also called interactive server-side rendering, interactive
SSR) to generate interactive components with prerendering on the server.
Interactive WebAssembly rendering (also called client-side rendering, CSR, which is
always assumed to be interactive) to generate interactive components on the client
with prerendering on the server.
Interactive Auto (automatic) rendering to initially use the server-side ASP.NET Core
runtime for content rendering and interactivity. The .NET WebAssembly runtime on
the client is used for subsequent rendering and interactivity after the Blazor bundle
is downloaded and the WebAssembly runtime activates. Interactive Auto rendering
usually provides the fastest app startup experience.
Examples throughout the Blazor documentation have been updated for use in Blazor
Web Apps. Blazor Server examples remain in content versioned for .NET 7 or earlier.
For more information, see ASP.NET Core Razor class libraries (RCLs) with static server-
side rendering (static SSR).
For more information, see Avoid HTTP caching issues when upgrading ASP.NET Core
Blazor apps.
As part of unifying the various Blazor hosting models into a single model in .NET 8,
we're also consolidating the number of Blazor project templates. We removed the Blazor
Server template, and the ASP.NET Core Hosted option has been removed from the
Blazor WebAssembly template. Both of these scenarios are represented by options when
using the Blazor Web App template.
7 Note
Existing Blazor Server and Blazor WebAssembly apps remain supported in .NET 8.
Optionally, these apps can be updated to use the new full-stack web UI Blazor
features.
For more information on the new Blazor Web App template, see the following articles:
beforeStart is used for tasks such as customizing the loading process, logging
The preceding legacy JS initializers aren't invoked by default in a Blazor Web App. For
Blazor Web Apps, a new set of JS initializers are used: beforeWebStart , afterWebStarted ,
beforeServerStart , afterServerStarted , beforeWebAssemblyStart , and
afterWebAssemblyStarted .
Blazor Web Apps automatically persist any registered app-level state created during
prerendering, removing the need for the Persist Component State Tag Helper.
the model.
New enhanced navigation API allows you to refresh the current page by calling
NavigationManager.Refresh(bool forceLoad = false) .
For more information, see the following sections of the Blazor Routing article:
Streaming rendering
You can now stream content updates on the response stream when using static server-
side rendering (static SSR) with Blazor. Streaming rendering can improve the user
experience for pages that perform long-running asynchronous tasks in order to fully
render by rendering content as soon as it's available.
For example, to render a page you might need to make a long running database query
or an API call. Normally, asynchronous tasks executed as part of rendering a page must
complete before the rendered response is sent, which can delay loading the page.
Streaming rendering initially renders the entire page with placeholder content while
asynchronous operations execute. After the asynchronous operations are complete, the
updated content is sent to the client on the same response connection and patched by
into the DOM. The benefit of this approach is that the main layout of the app renders as
quickly as possible and the page is updated as soon as the content is ready.
C#
[Inject(Key = "my-service")]
public IMyService MyService { get; set; }
The @inject Razor directive doesn't support keyed services for this release, but work is
tracked by Update @inject to support keyed services (dotnet/razor #9286) for a future
.NET release.
[CascadingParameter]
public HttpContext? HttpContext { get; set; }
Accessing the HttpContext from a static server component might be useful for
inspecting and modifying headers or other properties.
For an example that passes HttpContext state, access and refresh tokens, to
components, see Server-side ASP.NET Core Blazor additional security scenarios.
For more information, see Render Razor components outside of ASP.NET Core.
Sections support
The new SectionOutlet and SectionContent components in Blazor add support for
specifying outlets for content that can be filled in later. Sections are often used to define
placeholders in layouts that are then filled in by specific pages. Sections are referenced
either by a unique name or using a unique object ID.
QuickGrid
The Blazor QuickGrid component is no longer experimental and is now part of the
Blazor framework in .NET 8.
QuickGrid is a high performance grid component for displaying data in tabular form.
QuickGrid is built to be a simple and convenient way to display your data, while still
providing powerful features, such as sorting, filtering, paging, and virtualization.
For more information, see ASP.NET Core Blazor routing and navigation.
For more information, see ASP.NET Core Blazor cascading values and parameters.
any activity sent from the browser to the server, such as UI events or JavaScript-to-.NET
interop calls.
For more information, see Host and deploy ASP.NET Core Blazor WebAssembly.
For more information, see Host and deploy ASP.NET Core Blazor WebAssembly.
7 Note
Prior to the release of .NET 8, guidance in Deployment layout for ASP.NET Core
hosted Blazor WebAssembly apps addresses environments that block clients from
downloading and executing DLLs with a multipart bundling approach. In .NET 8 or
later, Blazor uses the Webcil file format to address this problem. Multipart bundling
using the experimental NuGet package described by the WebAssembly deployment
layout article isn't supported for Blazor apps in .NET 8 or later. For more
information, see Enhance
Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle package to
define a custom bundle format (dotnet/aspnetcore #36978) . If you desire to
continue using the multipart bundle package in .NET 8 or later apps, you can use
the guidance in the article to create your own multipart bundling NuGet package,
but it won't be supported by Microsoft.
You can now debug Blazor WebAssembly apps using Firefox. Debugging Blazor
WebAssembly apps requires configuring the browser for remote debugging and then
connecting to the browser using the browser developer tools through the .NET
WebAssembly debugging proxy. Debugging Firefox from Visual Studio isn't supported
at this time.
For more information, see Enforce a Content Security Policy for ASP.NET Core Blazor.
For more information, see Handle errors in ASP.NET Core Blazor apps.
Configure the .NET WebAssembly runtime
The .NET WebAssembly runtime can now be configured for Blazor startup.
Prior workarounds for configuring hub connection timeouts can be replaced with formal
SignalR hub connection builder timeout configuration.
OnClose is called when the my-dialog dialog is closed with the Close button.
OnCancel is called when the dialog is canceled with the Esc key. When an HTML
dialog is dismissed with the Esc key, both the cancel and close events are
triggered.
razor
<div>
<p>Output: @message</p>
<button onclick="document.getElementById('my-dialog').showModal()">
Show modal dialog
</button>
@code {
private string? message;
Blazor Identity UI
Blazor supports generating a full Blazor-based Identity UI when you choose the
authentication option for Individual Accounts. You can either select the option for
Individual Accounts in the new project dialog for Blazor Web Apps from Visual Studio or
pass the -au|--auth option set to Individual from the command line when you create
a new project.
For more information, see Migrate from ASP.NET Core 7.0 to 8.0.
For more information, see Support for multiple Blazor Web apps per server project
(dotnet/aspnetcore #52216) .
Blazor Hybrid
The following articles document changes for Blazor Hybrid in .NET 8:
Troubleshoot ASP.NET Core Blazor Hybrid: A new article explains how to use
BlazorWebView logging.
Build a .NET MAUI Blazor Hybrid app: The project template name .NET MAUI
Blazor has changed to .NET MAUI Blazor Hybrid.
ASP.NET Core Blazor Hybrid: BlazorWebView gains a TryDispatchAsync method that
calls a specified Action<ServiceProvider> asynchronously and passes in the scoped
services available in Razor components. This enables code from the native UI to
access scoped services such as NavigationManager .
ASP.NET Core Blazor Hybrid routing and navigation: Use the
BlazorWebView.StartPath property to get or set the path for initial navigation
within the Blazor navigation context when the Razor component is finished
loading.
diff
- [Parameter]
[SupplyParameterFromQuery]
SignalR
The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:
JavaScript
connection.serverTimeoutInMilliseconds = 60000;
connection.keepAliveIntervalInMilliseconds = 30000;
JavaScript
The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:
JavaScript
Blazor.start({
configureSignalR: function (builder) {
let c = builder.build();
c.serverTimeoutInMilliseconds = 60000;
c.keepAliveIntervalInMilliseconds = 30000;
builder.build = () => {
return c;
};
}
});
JavaScript
Blazor.start({
circuit: {
configureSignalR: function (builder) {
builder.withServerTimeout(60000).withKeepAliveInterval(30000);
}
}
});
Blazor Server:
JavaScript
Blazor.start({
configureSignalR: function (builder) {
builder.withServerTimeout(60000).withKeepAliveInterval(30000);
}
});
C#
builder.ServerTimeout = TimeSpan.FromSeconds(60);
builder.KeepAliveInterval = TimeSpan.FromSeconds(30);
C#
await builder.StartAsync();
Opt in to stateful reconnect at both the server hub endpoint and the client:
C#
Optionally, the maximum buffer size in bytes allowed by the server can be set
globally or for a specific hub with the StatefulReconnectBufferSize option:
C#
C#
builder.AddSignalR().AddHubOptions<MyHub>(o =>
o.StatefulReconnectBufferSize = 1000);
JavaScript
C#
Minimal APIs
This section describes new features for minimal APIs. See also the section on Native AOT
for more information relevant to minimal APIs.
C#
app.UseRequestLocalization(options =>
{
options.CultureInfoUseUserOverride = false;
});
Binding to forms
Explicit binding to form values using the [FromForm] attribute is now supported.
Parameters bound to the request with [FromForm] include an antiforgery token. The
antiforgery token is validated when the request is processed.
For more information, see Bind to collections and complex types from forms.
C#
builder.Services.AddAntiforgery();
app.UseAntiforgery();
app.Run();
Does not short-circuit the execution of the rest of the request pipeline.
Sets the IAntiforgeryValidationFeature in the HttpContext.Features of the current
request.
The HTTP method associated with the endpoint is a relevant HTTP method . The
relevant methods are all HTTP methods except for TRACE, OPTIONS, HEAD, and
GET.
The request is associated with a valid endpoint.
In this release, we've made the object pool easier to use by adding the IResettable
interface. Reusable types often need to be reset back to a default state between uses.
IResettable types are automatically reset when returned to an object pool.
Native AOT
Support for .NET native ahead-of-time (AOT) has been added. Apps that are published
using AOT can have substantially better performance: smaller app size, less memory
usage, and faster startup time. Native AOT is currently supported by gRPC, minimal API,
and worker service apps. For more information, see ASP.NET Core support for Native
AOT and Tutorial: Publish an ASP.NET Core app using Native AOT. For information about
known issues with ASP.NET Core and Native AOT compatibility, see GitHub issue
dotnet/core #8288 .
Libraries using these dynamic features need to be updated in order to work with Native
AOT. They can be updated using tools like Roslyn source generators.
C#
Console.WriteLine("Running...");
app.Run();
Publishing this code with Native AOT using .NET 8 Preview 7 on a linux-x64 machine
results in a self-contained native executable of about 8.5 MB.
Reduced app size with configurable HTTPS support
We've further reduced Native AOT binary size for apps that don't need HTTPS or HTTP/3
support. Not using HTTPS or HTTP/3 is common for apps that run behind a TLS
termination proxy (for example, hosted on Azure). The new
WebApplication.CreateSlimBuilder method omits this functionality by default. It can be
New features were added to System.Text.Json to better support Native AOT. These new
features add capabilities for the source generation mode of System.Text.Json , because
reflection isn't supported by AOT.
This API is useful in scenarios where a route handler uses yield return to
asynchronously return an enumeration. For example, to materialize rows from a
database query. For more information, see Unspeakable type support in the .NET 8
Preview 4 announcement.
We're working to ensure that as many as possible of the Minimal API features are
supported by the RDG and thus compatible with Native AOT.
The RDG is enabled automatically in a project when publishing with Native AOT is
enabled. RDG can be manually enabled even when not using Native AOT by setting
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator> in the project
file. This can be useful when initially evaluating a project's readiness for Native AOT, or
to reduce the startup time of an app.
too. Native AOT compatibility requires the use of the System.Text.Json source
generator. All types accepted as parameters to or returned from request delegates in
Minimal APIs must be configured on a JsonSerializerContext that is registered via
ASP.NET Core's dependency injection, for example:
C#
...
// Add types used in the minimal API app to source generated JSON serializer
content
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
For more information about the TypeInfoResolverChain API, see the following resources:
JsonSerializerOptions.TypeInfoResolverChain
Chain source generators
Changes to support source generation
Library authors wishing to learn more about preparing their libraries for Native AOT are
encouraged to start by preparing their library for trimming and learning more about the
Native AOT compatibility requirements.
C#
For more information about this feature and how to use .NET and gRPC to create an IPC
server and client, see Inter-process communication with gRPC.
certificate file is treated the same way as a change to the configured path (that is,
endpoints are reloaded).
Note that file deletions are specifically not tracked since they arise transiently and would
crash the server if non-transient.
Applications and containers are often only given a port to listen on, like 80, without
additional constraints like host or path. HTTP_PORTS and HTTPS_PORTS are new config
keys that allow specifying the listening ports for the Kestrel and HTTP.sys servers. These
can be defined with the DOTNET_ or ASPNETCORE_ environment variable prefixes, or
specified directly through any other config input like appsettings.json. Each is a
semicolon delimited list of port values. For example:
cli
ASPNETCORE_HTTP_PORTS=80;8080
ASPNETCORE_HTTPS_PORTS=443;8081
This is shorthand for the following, which specifies the scheme (HTTP or HTTPS) and any
host or IP:
cli
ASPNETCORE_URLS=http://*:80/;http://*:8080/;https://*:443/;https://*:8081/
For more information, see Configure endpoints for the ASP.NET Core Kestrel web server
and HTTP.sys web server implementation in ASP.NET Core.
SNI is part of the TLS handshake process. It allows clients to specify the host name
they're attempting to connect to when the server hosts multiple virtual hosts or
domains. To present the correct security certificate during the handshake process, the
server needs to know the host name selected for each request.
Normally the host name is only handled within the TLS stack and is used to select the
matching certificate. But by exposing it, other components in an app can use that
information for purposes such as diagnostics, rate limiting, routing, and billing.
Exposing the host name is useful for large-scale services managing thousands of SNI
bindings. This feature can significantly improve debugging efficiency during customer
escalations. The increased transparency allows for faster problem resolution and
enhanced service reliability.
IHttpSysRequestTimingFeature
IHttpSysRequestTimingFeature provides detailed timing information for requests
when using the HTTP.sys server and In-process hosting with IIS:
C#
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseHttpSys();
var loggerFactory =
context.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Sample");
return next(context);
});
app.Run();
Apps that use asynchronous I/O and that can have more than one write outstanding at a
time should not use this flag. Enabling this flag can result in higher CPU and memory
usage by HTTP.Sys.
IAuthorizationRequirementData
Prior to ASP.NET Core 8, adding a parameterized authorization policy to an endpoint
required implementing an:
contract.
AuthorizationRequirement for the policy.
For example, consider the following sample written for ASP.NET Core 7.0:
C#
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
app.MapControllers();
app.Run();
C#
using Microsoft.AspNetCore.Mvc;
namespace AuthRequirementsData.Controllers;
[ApiController]
[Route("api/[controller]")]
public class GreetingsController : Controller
{
[MinimumAgeAuthorize(16)]
[HttpGet("hello")]
public string Hello() => $"Hello {(HttpContext.User.Identity?.Name ??
"world")}!";
}
C#
using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;
namespace AuthRequirementsData.Authorization;
class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeRequirement>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;
public
MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler>
logger)
{
_logger = logger;
}
requirement.Age);
ClaimTypes.DateOfBirth);
if (dateOfBirthClaim != null)
{
// If the user has a date of birth claim, check their age
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value,
CultureInfo.InvariantCulture);
var age = DateTime.Now.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Now.AddYears(-age))
{
// Adjust age if the user hasn't had a birthday yet this
year.
age--;
}
return Task.CompletedTask;
}
}
diff
using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
- builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();
app.MapControllers();
app.Run();
diff
using Microsoft.AspNetCore.Authorization;
using System.Globalization;
using System.Security.Claims;
namespace AuthRequirementsData.Authorization;
- class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeRequirement>
+ class MinimumAgeAuthorizationHandler :
AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
private readonly ILogger<MinimumAgeAuthorizationHandler> _logger;
public
MinimumAgeAuthorizationHandler(ILogger<MinimumAgeAuthorizationHandler>
logger)
{
_logger = logger;
}
Miscellaneous
The following sections describe miscellaneous new features in ASP.NET Core 8.
C#
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();
smallCache.Get("date"));
app.MapControllers();
app.Run();
[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache
cache)
{
return cache.Get("data-mvc");
}
}
Create a Visual Studio solution with a frontend project and a backend project.
Use the Visual Studio project type for JavaScript and TypeScript (.esproj) for the
frontend.
Use an ASP.NET Core project for the backend.
For more information about the Visual Studio templates and how to access the legacy
templates, see Overview of Single Page Apps (SPAs) in ASP.NET Core
diff
[ApiController]
[Route("api/[controller]")]
public class TodosController : Controller
{
[HttpGet("/")]
- [ProducesResponseType(typeof(Todo), StatusCodes.Status200OK)]
+ [ProducesResponseType<Todo>(StatusCodes.Status200OK)]
public Todo Get() => new Todo(1, "Write a sample", DateTime.Now, false);
}
[ProducesResponseType<T>]
[Produces<T>]
[MiddlewareFilter<T>]
[ModelBinder<T>]
[ModelMetadataType<T>]
[ServiceFilter<T>]
[TypeFilter<T>]
ノ Expand table
Route tooling
ASP.NET Core is built on routing. Minimal APIs, Web APIs, Razor Pages, and Blazor all
use routes to customize how HTTP requests map to code.
In .NET 8 we've invested in a suite of new features to make routing easier to learn and
use. These new features include:
Metrics have been added for ASP.NET Core hosting, Kestrel, and SignalR. For more
information, see System.Diagnostics.Metrics.
IExceptionHandler
IExceptionHandler is a new interface that gives the developer a callback for handling
known exceptions in a central location.
debugger displays for these types make finding important information easier in an IDE's
debugger. The following screenshots show the difference that these attributes make in
the debugger's display of HttpContext .
.NET 7:
.NET 8:
.NET 7:
.NET 8:
The new Parse and TryParse methods on IPNetwork add support for creating an
IPNetwork by using an input string in CIDR notation or "slash notation".
C#
// Using Parse
var network = IPNetwork.Parse("192.168.0.1/32");
C#
// Using TryParse
bool success = IPNetwork.TryParse("192.168.0.1/32", out var network);
C#
// Constructor equivalent
var network = new IPNetwork(IPAddress.Parse("192.168.0.1"), 32);
C#
// Using Parse
var network = IPNetwork.Parse("2001:db8:3c4d::1/128");
C#
// Using TryParse
bool success = IPNetwork.TryParse("2001:db8:3c4d::1/128", out var network);
C#
// Constructor equivalent
var network = new IPNetwork(IPAddress.Parse("2001:db8:3c4d::1"), 128);
C#
Use the MapShortCircuit method to set up short-circuiting for multiple routes at once,
by passing to it a params array of URL prefixes. For example, browsers and bots often
probe servers for well known paths like robots.txt and favicon.ico . If the app doesn't
have those files, one line of code can configure both routes:
C#
For more information, see HTTP logging in .NET Core and ASP.NET Core.
C#
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async httpContext =>
{
var pds =
httpContext.RequestServices.GetService<IProblemDetailsService>();
if (pds == null
|| !await pds.TryWriteAsync(new() { HttpContext = httpContext
}))
{
// Fallback behavior
await httpContext.Response.WriteAsync("Fallback: An error
occurred.");
}
});
});
app.MapGet("/exception", () =>
{
throw new InvalidOperationException("Sample Exception");
});
app.Run();
Additional resources
Announcing ASP.NET Core in .NET 8 (blog post)
ASP.NET Core announcements and breaking changes (aspnet/Announcements
GitHub repository)
.NET announcements and breaking changes (dotnet/Announcements GitHub
repository)
What's new in Windows Forms for .NET
8
Article • 11/11/2024
This article describes some of the new Windows Forms features and enhancements in
.NET 8.
There are a few breaking changes you should be aware of when migrating from .NET
Framework to .NET 8. For more information, see Breaking changes in Windows Forms.
The enhanced data binding capabilities make it simpler to fully utilize the MVVM pattern
and employ object-relational mappers from ViewModels in Windows Forms. This
reduces the amount of code in code-behind files. More importantly, it enables code
sharing between Windows Forms and other .NET GUI frameworks like WPF, UWP/WinUI,
and .NET MAUI. It's important to note that while the previously mentioned GUI
frameworks use XAML as a UI technology, XAML isn't coming to Windows Forms.
The IBindableComponent interface and the BindableComponent class drive the new
binding system. Control implements the interface and provides new data binding
capabilities to Windows Forms.
Button commands
Button commands were in preview with .NET 7, and is now fully enabled in .NET 8.
Similar to WPF, the instance of an object that implements the ICommand interface can
be assigned to the button's Command property. When the button is clicked, the
command is invoked.
The Command and CommandParameter properties are set in the designer through the
Properties window, under (DataBindings), as illustrated by the following image.
Buttons also listen to the ICommand.CanExecuteChanged event, which causes the
control to query the ICommand.CanExecute method. When that method returns true ,
the control is enabled; the control is disabled when false is returned.
You can enable the DPI-unaware designer for the Windows Forms project by adding
<ForceDesignerDPIUnaware> to the project file, and setting the value to true .
XML
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<ForceDesignerDPIUnaware>true</ForceDesignerDPIUnaware>
<ApplicationHighDpiMode>DpiUnawareGdiScaled</ApplicationHighDpiMode>
</PropertyGroup>
) Important
Visual Studio reads this setting when the project is loaded, and not when it's
changed. After changing this setting, unload and reload your project to get Visual
Studio to respect it.
Correctly scale nested controls. For example, a button that's in a panel, which is
placed on a tab page.
Starting with .NET 8, this feature is enabled by default and you need to opt out of
it to revert to the previous behavior.
JSON
{
"runtimeOptions": {
"tfm": "net8.0",
"frameworks": [
...
],
"configProperties": {
"System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi": false,
}
}
}
Miscellaneous improvements
Here are some other notable changes:
The code that handled FolderBrowserDialog was improved, fixing a few memory
leaks.
The code base for Windows Forms has been slowly enabling C# nullability, rooting
out any potential null-reference errors.
The System.Drawing source code was migrated to the Windows Forms GitHub
repository .
Modern Windows icons can be accessed by a new API,
System.Drawing.SystemIcons.GetStockIcon. The System.Drawing.StockIconId
enumeration lists all of the available system icons.
More designers are available at run-time now. For more information, see GitHub
issue #4908 .
What's new in .NET MAUI for .NET 8
Article • 05/07/2024
The focus of .NET MAUI in .NET 8 is quality. In .NET 8, 1618 pull requests were merged
that closed 689 issues. These includes changes from the .NET MAUI team as well as the
.NET MAUI community. These changes should result in a significant increase in quality in
.NET 8.
) Important
In .NET 8, .NET MAUI ships as a .NET workload and multiple NuGet packages. The
advantage of this approach is that it enables you to easily pin your projects to specific
versions, while also enabling you to easily preview unreleased or experimental builds.
When you create a new .NET MAUI project the required NuGet packages are
automatically added to the project.
This article lists the new features of .NET MAUI for .NET 8 and provides links to more
detailed information on each.
For information about what's new in .NET 8, see What's new in .NET 8.
New functionality
While the focus of this release of .NET MAUI is quality, there's also some new
functionality that enables new scenarios in your apps.
Controls
Controls that support text input gain extension methods that support hiding and
showing the soft input keyboard. For more information, see Hide and show the
soft input keyboard.
The ContentPage class gains a HideSoftInputOnTapped property, which indicates
whether tapping anywhere on the page will cause the soft input keyboard to hide
if it's visible. For more information, see ContentPage.
BlazorWebView gains a StartPath property, a TryDispatchAsync method, and
enhanced logging capabilities. For more information, see Host a Blazor web app in
a .NET MAUI app using BlazorWebView.
WebView gains a UserAgent property. For more information, see WebView.
Inline media playback of HTML5 video, including autoplay and picture in picture,
has been enabled by default for the WebView on iOS. For more information, see
Set media playback preferences on iOS and Mac Catalyst.
The Grid.Add overload that accepts 5 arguments has been added back to .NET
MAUI. However, this method is deprecated and is only present to aid migrations
from Xamarin.Forms.
Grid gains an AddWithSpan extension method that adds a view to the Grid at the
specified row and column with the specified row and column spans.
Desktop
Menu bar items and context menu items can be invoked through keyboard
shortcuts known as keyboard accelerators. For more information, see Keyboard
accelerators.
Windows apps can be published as unpackaged apps. For more information, see
Publish an unpackaged .NET MAUI app for Windows with the CLI.
Gesture recognizers
PointerGestureRecognizer gains PointerPressedCommand,
PointerPressedCommandParameter, PointerReleasedCommand,
PointerReleasedCommandParameter properties, and PointerPressed and
PointerReleased events. For more information, see Recognize a pointer gesture.
The PointerEventArgs object that accompanies the pointer events raised by the
PointerGestureRecognizer class gains a PlatformArgs property of type
PlatformPointerEventArgs. This property provides access to the platform-specific
arguments for a pointer gesture event. For more information, see Recognize a
pointer gesture.
The DragStartingEventArgs, DragEventArgs, DropEventArgs, and
DropCompletedEventArgs objects that accompany drag and drop gesture events
each gain a PlatformArgs property. This property provides access to the platform-
specific arguments for a drag or drop event. For more information, see Recognize
a drag and drop gesture.
The position at which a drag or drop gesture occurred can be obtained by calling
the GetPosition method on a DragEventArgs, DragStartingEventArgs, or
DropEventArgs object. For more information, see Recognize a drag and drop
gesture.
The TapGestureRecognizer class gains the ability to handle secondary taps on
Android. For more information, see Recognize a tap gesture.
Navigation
Shell navigation gains a GoToAsync overload that enables you to pass single use
navigation data, that's cleared after navigation has occurred, as a
ShellNavigationQueryParameters object. For more information, see Pass single use
object-based navigation data.
Platform integration
The Geolocation class can listen for location changes when app's are in the
foreground. For more information, see Listen for location changes.
Flashlight gains a IsSupportedAsync method that determines whether a flashlight
is available on the device. For more information, see Flashlight.
SensorSpeed intervals have been unified across all platforms. For more
information, see Accessing device sensors.
The Permissions class gains the Permissions.Bluetooth permission, which is an
Android 12 permission for looking for Bluetooth devices, making the current
device discoverable to other Bluetooth devices, and communicating with already-
paired Bluetooth devices. For more information, see Permissions.
The Permissions class gains the Permissions.NearbyWifiDevices permission, which
is an Android 13 permission for accessing nearby WiFi devices. For more
information, see Permissions.
XAML
The x:ClassModifier attribute can be specified on XAML classes, to control the
access level for a generated class in an assembly. For more information, see Class
modifiers.
Resources defined in a ResourceDictionary can also be consumed in an
AppThemeBinding with the DynamicResource markup extension. For more
information, see Define and consume theme resources.
Color is the ContentProperty of the SolidColorBrush class, and therefore does not
need to be explicitly set from XAML.
Troubleshooting
For troubleshooting purposes, resource generation can be disabled. For more
information, see Disable image packaging, Disable splash screen packaging,
Disable font packaging, and Disable asset file packaging.
For troubleshooting purposes, a blank splash screen can be generated. For more
information, see Generate a blank splash screen.
Resizeter checks for duplicate image filenames. For more information, see
Duplicate image filename errors.
Miscellaneous
Window management can be decoupled from the App class. For more information,
see Decouple window management from the App class.
Several system fonts can be easily consumed in Android apps. For more
information, see Consume fonts.
On iOS, MauiUIApplicationDelegate gains a PerformFetch method that can be
overridden or consumed via the iOSLifecycle.PerformFetch delegate. For more
information, see iOS and Mac Catalyst platform lifecycle events.
Behavior changes
The following behavior has changed from the previous release:
Use of the Map control from XAML now requires the following xmlns namespace
declaration: xmlns:maps="http://schemas.microsoft.com/dotnet/2021/maui/maps" .
Image caching is disabled on Android when loading an image from a stream with
the ImageSource.FromStream method. This is due to the lack of data from which to
create a reasonable cache key.
On iOS, pages automatically scroll when the soft input keyboard would cover a text
entry field, so that the field is above the soft input keyboard. The
KeyboardAutoManagerScroll.Disconnect method, in the Microsoft.Maui.Platform
Performance
There are plenty of performance changes in .NET MAUI 8. These changes can be
classified into five areas:
New features
AndroidStripILAfterAOT
AndroidEnableMarshalMethods
NativeAOT on iOS
Build and inner loop performance
Filter Android ps -A output with grep
Port WindowsAppSDK usage of vcmeta.dll to C#
Improvements to remote iOS builds on Windows
Improvements to Android inner-loop
XAML Compilation no longer uses LoadInSeparateAppDomain
Performance or app size improvements
Structs and IEquatable in .NET MAUI
Fix performance issue in {AppThemeBinding}
Address CA1307 and CA1309 for performance
Address CA1311 for performance
Remove unused ViewAttachedToWindow event on Android
Remove unneeded System.Reflection for {Binding}
Use StringComparer.Ordinal for Dictionary and HashSet
Reduce Java interop in MauiDrawable on Android
Improve layout performance of Label on Android
Reduce Java interop calls for controls in .NET MAUI
Improve performance of Entry.MaxLength on Android
Improve memory usage of CollectionView on Windows
Use UnmanagedCallersOnlyAttribute on Apple platforms
Faster Java interop for strings on Android
Faster Java interop for C# events on Android
Use Function Pointers for JNI
Removed Xamarin.AndroidX.Legacy.Support.V4
Deduplication of generics on iOS and macOS
Fix System.Linq.Expressions implementation on iOS-like platforms
Set DynamicCodeSupport=false for iOS and Catalyst
Memory leaks
Memory Leaks and Quality
Diagnosing leaks in .NET MAUI
Patterns that cause leaks: C# events
Circular references on Apple platforms
Roslyn analyzer for Apple platforms
Tooling and documentation
Simplified dotnet-trace and dotnet-dsrouter
dotnet-gcdump Support for Mobile
Then, open your .csproj file and change the Target Framework Monikers (TFMs) from 7
to 8. If you're using a TFM such as net7.0-ios13.6 be sure to match the platform
version or remove it entirely. The following example shows the TFMs for a .NET 7
project:
XML
<TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst;net7.0-
tizen</TargetFrameworks>
<TargetFrameworks
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0
-windows10.0.19041.0</TargetFrameworks>
XML
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-
tizen</TargetFrameworks>
<TargetFrameworks
Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0
-windows10.0.19041.0</TargetFrameworks>
Explicit package references should also be added to your .csproj file for the following
.NET MAUI NuGet packages:
XML
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls"
Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility"
Version="$(MauiVersion)" />
</ItemGroup>
The $(MauiVersion) variable is referenced from the version of .NET MAUI you've
installed. You can override this by adding the $(MauiVersion) build property to your
.csproj file:
XML
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-
maccatalyst</TargetFrameworks>
<UseMaui>True</UseMaui>
<MauiVersion>8.0.3</MauiVersion>
</PropertyGroup>
</Project>
This can be useful when using ad-hoc builds from the nightly feed or builds
downloaded from pull requests.
In addition, the $(ApplicationIdGuid) build property can be removed from your .csproj
file in .NET 8. For more information, see Behavior changes.
Prior to building your upgraded app for the first time, delete the bin and obj folders.
7 Note
The project template for a .NET MAUI app in .NET 8 enables the nullable context for
the project with the $(Nullable) build property. For more information, see
Nullable.
See also
Release notes for .NET MAUI
Release notes for .NET for iOS, tvOS, macOS, and Mac Catalyst
Release notes for .NET for Android
What's New in EF Core 8
Article • 11/14/2023
Tip
You can run and debug into the samples by downloading the sample code from
GitHub . Each section links to the source code specific to that section.
EF8 requires the .NET 8 SDK to build and requires the .NET 8 runtime to run. EF8 will
not run on earlier .NET versions, and will not run on .NET Framework.
Objects that are unstructured and hold a single value. For example, int , Guid ,
string , IPAddress . These are (somewhat loosely) called "primitive types".
Objects that are structured to hold multiple values, and where the identity of the
object is defined by a key value. For example, Blog , Post , Customer . These are
called "entity types".
Objects that are structured to hold multiple values, but the object has no key
defining identity. For example, Address , Coordinate .
Prior to EF8, there was no good way to map the third type of object. Owned types can
be used, but since owned types are actually entity types, they have semantics based on
a key value, even when that key value is hidden.
EF8 now supports "Complex Types" to cover this third type of object. Complex type
objects:
Simple example
For example, consider an Address type:
C#
C#
C#
context.Add(customer);
await context.SaveChangesAsync();
This results in the following row being inserted into the database:
SQL
Notice that the complex types do not get their own tables. Instead, they are saved inline
to columns of the Customers table. This matches the table sharing behavior of owned
types.
7 Note
We don't plan to allow complex types to be mapped to their own table. However,
in a future release, we do plan to allow the complex type to be saved as a JSON
document in a single column. Vote for Issue #31252 if this is important to you.
Now let's say we want to ship an order to a customer and use the customer's address as
both the default billing an shipping address. The natural way to do this is to copy the
Address object from the Customer into the Order . For example:
C#
customer.Orders.Add(
new Order { Contents = "Tesco Tasty Treats", BillingAddress =
customer.Address, ShippingAddress = customer.Address, });
await context.SaveChangesAsync();
With complex types, this works as expected, and the address is inserted into the Orders
table:
SQL
So far you might be saying, "but I could do this with owned types!" However, the "entity
type" semantics of owned types quickly get in the way. For example, running the code
above with owned types results in a slew of warnings and then an error:
text
This is because a single instance of the Address entity type (with the same hidden key
value) is being used for three different entity instances. On the other hand, sharing the
same instance between complex properties is allowed, and so the code works as
expected when using complex types.
For example, the Address type can be configured using the ComplexTypeAttribute:
C#
[ComplexType]
public class Address
{
public required string Line1 { get; set; }
public string? Line2 { get; set; }
public required string City { get; set; }
public required string Country { get; set; }
public required string PostCode { get; set; }
}
Or in OnModelCreating :
C#
modelBuilder.Entity<Order>(b =>
{
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Mutability
In the example above, we ended up with the same Address instance used in three
places. This is allowed and doesn't cause any issues for EF Core when using complex
types. However, sharing instances of the same reference type means that if a property
value on the instance is modified, then that change will be reflected in all three usages.
For example, following on from above, let's change Line1 of the customer address and
save the changes:
C#
This results in the following update to the database when using SQL Server:
SQL
UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Orders] SET [BillingAddress_Line1] = @p2, [ShippingAddress_Line1] =
@p3
OUTPUT 1
WHERE [Id] = @p4;
Notice that all three Line1 columns have changed, since they are all sharing the same
instance. This is usually not what we want.
Tip
A good way to deal with issues like this is to make the type immutable. Indeed, this
immutability is often natural when a type is a good candidate for being a complex type.
For example, it usually makes sense to supply a complex new Address object rather than
to just mutate, say, the country while leaving the rest the same.
Both reference and value types can be made immutable. We'll look at some examples in
the following sections.
Immutable class
We used a simple, mutable class in the example above. To prevent the issues with
accidental mutation described above, we can make the class immutable. For example:
C#
Tip
C#
It is now not possible to change the Line1 value on an existing address. Instead, we
need to create a new instance with the changed value. For example:
C#
await context.SaveChangesAsync();
This time the call to SaveChangesAsync only updates the customer address:
SQL
Note that even though the Address object is immutable, and the entire object has been
changed, EF is still tracking changes to the individual properties, so only the columns
with changed values are updated.
Immutable record
C# 9 introduced record types, which makes creating and using immutable objects easier.
For example, the Address object can be made a record type:
C#
Tip
C#
Replacing the mutable object and calling SaveChanges now requires less code:
C#
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();
Mutable struct
A simple mutable value type can be used as a complex type. For example, Address can
be defined as a struct in C#:
C#
Assigning the customer Address object to the shipping and billing Address properties
results in each property getting a copy of the Address , since this is how value types
work. This means that modifying the Address on the customer will not change the
shipping or billing Address instances, so mutable structs don't have the same instance-
sharing issues that happen with mutable classes.
However, mutable structs are generally discouraged in C#, so think very carefully before
using them.
Immutable struct
Immutable structs work well as complex types, just like immutable classes do. For
example, Address can be defined such that it can not be modified:
C#
The code for changing the address now looks the same as when using immutable class:
C#
await context.SaveChangesAsync();
C# 10 introduced struct record types, which makes it easy to create and work with
immutable struct records like it is with immutable class records. For example, we can
define Address as an immutable struct record:
C#
The code for changing the address now looks the same as when using immutable class
record:
C#
await context.SaveChangesAsync();
C#
public record Address(string Line1, string? Line2, string City, string
Country, string PostCode);
We're using immutable records here, since these are a good match for the semantics of
our complex types, but nesting of complex types can be done with any flavor of .NET
type.
7 Note
We're not using a primary constructor for the Contact type because EF Core does
not yet support constructor injection of complex type values. Vote for Issue
#31621 if this is important to you.
C#
C#
C#
[ComplexType]
public record Address(string Line1, string? Line2, string City, string
Country, string PostCode);
[ComplexType]
public record PhoneNumber(int CountryCode, long Number);
[ComplexType]
public record Contact
{
public required Address Address { get; init; }
public required PhoneNumber HomePhone { get; init; }
public required PhoneNumber WorkPhone { get; init; }
public required PhoneNumber MobilePhone { get; init; }
}
Or in OnModelCreating :
C#
modelBuilder.Entity<Order>(
b =>
{
b.ComplexProperty(e => e.ContactPhone);
b.ComplexProperty(e => e.BillingAddress);
b.ComplexProperty(e => e.ShippingAddress);
});
}
Queries
Properties of complex types on entity types are treated like any other non-navigation
property of the entity type. This means that they are always loaded when the entity type
is loaded. This is also true of any nested complex type properties. For example, querying
for a customer:
C#
SQL
Everything is returned to populate the customer and all the nested Contact ,
Address , and PhoneNumber complex types.
All the complex type values are stored as columns in the table for the entity type.
Complex types are never mapped to separate tables.
Projections
Complex types can be projected from a query. For example, selecting just the shipping
address from an order:
C#
var shippingAddress = await context.Orders
.Where(e => e.Id == orderId)
.Select(e => e.ShippingAddress)
.SingleAsync();
SQL
Note that projections of complex types cannot be tracked, since complex type objects
have no identity to use for tracking.
Use in predicates
Members of complex types can be used in predicates. For example, finding all the orders
going to a certain city:
C#
SQL
C#
SQL
Notice that equality is performed by expanding out each member of the complex type.
This aligns with complex types having no key for identity and hence a complex type
instance is equal to another complex type instance if and only if all their members are
equal. This also aligns with the equality defined by .NET for record types.
C#
A call to Property can be added to access a property of the complex type. For example
to get the current value of just the billing post code:
C#
Nested complex types are accessed using nested calls to ComplexProperty . For example,
to get the city from the nested Address of the Contact on a Customer :
C#
Other methods are available for reading and changing state. For example,
PropertyEntry.IsModified can be used to set a property of a complex type as modified:
C#
context.Entry(customer)
.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.PostCode)
.IsModified = true;
Current limitations
Complex types represent a significant investment across the EF stack. We were not able
to make everything work in this release, but we plan to close some of the gaps in a
future release. Make sure to vote (👍) on the appropriate GitHub issues if fixing any of
these limitations is important to you.
Primitive collections
A persistent question when using relational databases is what to do with collections of
primitive types; that is, lists or arrays of integers, date/times, strings, and so on. If you're
using PostgreSQL, then its easy to store these things using PostgreSQL's built-in array
type . For other databases, there are two common approaches:
Create a table with a column for the primitive type value and another column to
act as a foreign key linking each value to its owner of the collection.
Serialize the primitive collection into some column type that is handled by the
database--for example, serialize to and from a string.
The first option has advantages in many situations--we'll take a quick look at it at the
end of this section. However, it's not a natural representation of the data in the model,
and if what you really have is a collection of a primitive type, then the second option can
be more effective.
Starting with Preview 4, EF8 now includes built-in support for the second option, using
JSON as the serialization format. JSON works well for this since modern relational
databases include built-in mechanisms for querying and manipulating JSON, such that
the JSON column can, effectively, be treated as a table when needed, without the
overhead of actually creating that table. These same mechanisms allow JSON to be
passed in parameters and then used in similar way to table-valued parameters in
queries--more about this later.
Tip
C#
7 Note
SQL Server does not have native support for unsigned ints or URIs, but uint and
Uri are still treated as primitive types because there are built-in value converters
By default, EF Core uses an unconstrained Unicode string column type to hold the JSON,
since this protects against data loss with large collections. However, on some database
systems, such as SQL Server, specifying a maximum length for the string can improve
performance. This, along with other column configuration, can be done in the normal
way. For example:
C#
modelBuilder
.Entity<PrimitiveCollections>()
.Property(e => e.Booleans)
.HasMaxLength(1024)
.IsUnicode(false);
[MaxLength(2500)]
[Unicode(false)]
public uint[] UnsignedInts { get; set; }
A default column configuration can be used for all properties of a certain type using
pre-convention model configuration. For example:
C#
C#
Beers is an array of strings representing the beer brands available at the pub.
DaysVisited is a list of the dates on which the pub was visited.
Tip
In a real application, it would probably make more sense to create an entity type
for beer, and have a table for beers. We're showing a primitive collection here to
illustrate how they work. But remember, just because you can model something as
a primitive collection doesn't mean that you necessarily should.
The second entity type represents a dog walk in the British countryside:
C#
Like Pub , DogWalk also contains a collection of the dates visited, and a link to the closest
pub since, you know, sometimes the dog needs a saucer of beer after a long walk.
Using this model, the first query we will do is a simple Contains query to find all walks
with one of several different terrains:
C#
This is already translated by current versions of EF Core by inlining the values to look for.
For example, when using SQL Server:
SQL
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE [w].[Terrain] IN (1, 5, 4)
However, this strategy does not work well with database query caching; see Announcing
EF8 Preview 4 on the .NET Blog for a discussion of the issue.
) Important
The inlining of values here is done in such a way that there is no chance of a SQL
injection attack. The change to use JSON described below is all about performance,
and nothing to do with security.
For EF Core 8, the default is now to pass the list of terrains as a single parameter
containing a JSON collection. For example:
none
@__terrains_0='[1,5,4]'
SQL
SELECT [w].[Name]
FROM [Walks] AS [w]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__terrains_0) AS [t]
WHERE CAST([t].[value] AS int) = [w].[Terrain])
Or json_each on SQLite:
SQL
SELECT "w"."Name"
FROM "Walks" AS "w"
WHERE EXISTS (
SELECT 1
FROM json_each(@__terrains_0) AS "t"
WHERE "t"."value" = "w"."Terrain")
7 Note
OpenJson is only available on SQL Server 2016 (compatibility level 130) and later.
You can tell SQL Server that you're using an older version by configuring the
compatibility level as part of UseSqlServer . For example:
C#
Let's try a different kind of Contains query. In this case, we'll look for a value of the
parameter collection in the column. For example, any pub that stocks Heineken:
C#
The existing documentation from What's New in EF7 provides detailed information on
JSON mapping, queries, and updates. This documentation now also applies to SQLite.
SQL
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b]
WHERE [b].[value] = @__beer_0)
OpenJson is now used to to extract values from JSON column so that each value can be
We can combine the use of OpenJson on the parameter with OpenJson on the column.
For example, to find pubs that stock any one of a variety of lagers:
C#
SQL
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson(@__beers_0) AS [b]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[Beers]) AS [b0]
WHERE [b0].[value] = [b].[value] OR ([b0].[value] IS NULL AND [b].
[value] IS NULL)))
Let's look at a query that makes use of the column containing a collection of dates. For
example, to find pubs visited this year:
C#
SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d]
WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)
Notice that the query makes use of the date-specific function DATEPART here because EF
knows that the primitive collection contains dates. It might not seem like it, but this is
actually really important. Because EF knows what's in the collection, it can generate
appropriate SQL to use the typed values with parameters, functions, other columns etc.
Let's use the date collection again, this time to order appropriately for the type and
project values extracted from the collection. For example, let's list pubs in the order that
they were first visited, and with the first and last date each pub was visited:
C#
SQL
SELECT [p].[Name], (
SELECT TOP(1) CAST([d0].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d0]
ORDER BY CAST([d0].[value] AS date)) AS [FirstVisited], (
SELECT TOP(1) CAST([d1].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d1]
ORDER BY CAST([d1].[value] AS date) DESC) AS [LastVisited]
FROM [Pubs] AS [p]
ORDER BY (
SELECT TOP(1) CAST([d].[value] AS date)
FROM OpenJson([p].[DaysVisited]) AS [d]
ORDER BY CAST([d].[value] AS date))
And finally, just how often do we end up visiting the closest pub when taking the dog
for a walk? Let's find out:
C#
SQL
none
C#
Tip
We can now run a variation of our final query that, this time, extracts data from the
JSON document, including queries into the primitive collections contained in the
document:
C#
SQL
SQL
Tip
Notice that on SQLite EF Core now makes use of the ->> operator, resulting in
queries that are both easier to read and often more performant.
Mapping primitive collections to a table
We mentioned above that another option for primitive collections is to map them to a
different table. First class support for this is tracked by Issue #25163 ; make sure to
vote for this issue if it is important to you. Until this is implemented, the best approach
is to create a wrapping type for the primitive. For example, let's create a type for Beer :
C#
[Owned]
public class Beer
{
public Beer(string name)
{
Name = name;
}
Notice that the type simply wraps the primitive value--it doesn't have a primary key or
any foreign keys defined. This type can then be used in the Pub class:
C#
EF will now create a Beer table, synthesizing primary key and foreign key columns back
to the Pubs table. For example, on SQL Server:
SQL
Tip
C#
This translates into the following SQL when using SQL Server:
SQL
7 Note
This query will succeed even if a given post does not have any updates, or only has
a single update. In such a case, JSON_VALUE returns NULL and the predicate is not
matched.
Indexing into JSON arrays can also be used to project elements from an array into the
final results. For example, the following query projects out the UpdatedOn date for the
first and second updates of each post.
C#
This translates into the following SQL when using SQL Server:
SQL
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS
[LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS
[SecondLatestUpdate]
FROM [Posts] AS [p]
As noted above, JSON_VALUE returns null if the element of the array does not exist. This
is handled in the query by casting the projected value to a nullable DateOnly . An
alternative to casting the value is to filter the query results so that JSON_VALUE will never
return null. For example:
C#
This translates into the following SQL when using SQL Server:
SQL
SELECT [p].[Title],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) AS
[LatestUpdate],
CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) AS
[SecondLatestUpdate]
FROM [Posts] AS [p]
WHERE (CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS
date) IS NOT NULL)
AND (CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS
date) IS NOT NULL)
C#
var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search
#8", "Search #13", "Search #21", "Search #34" };
This translates into the following SQL when using SQL Server:
SQL
Mapping of aggregates built from .NET types to JSON documents stored in SQLite
columns
Queries into JSON columns, such as filtering and sorting by the elements of the
documents
Queries that project elements out of the JSON document into results
Updating and saving changes to JSON documents
The existing documentation from What's New in EF7 provides detailed information on
JSON mapping, queries, and updates. This documentation now also applies to SQLite.
Tip
The code shown in the EF7 documentation has been updated to also run on SQLite
can can be found in JsonColumnsSample.cs .
Queries into JSON columns on SQLite use the json_extract function. For example, the
"authors in Chigley" query from the documentation referenced above:
C#
SQL
C#
await context.SaveChangesAsync();
text
SQL
An organizational structure
A file system
A set of tasks in a project
A taxonomy of language terms
A graph of links between Web pages
The database is then able to run queries against this data using its hierarchical structure.
For example, a query can find ancestors and dependents of given items, or find all items
at a certain depth in the hierarchy.
Tip
The HierarchyId type is more idiomatic to the norms of .NET than SqlHierarchyId ,
which is instead modeled after how .NET Framework types are hosted inside the
SQL Server database engine. HierarchyId is designed to work with EF Core, but it
can also be used outside of EF Core in other applications. The
Microsoft.EntityFrameworkCore.SqlServer.Abstractions package doesn't reference
any other packages, and so has minimal impact on deployed application size and
dependencies.
Use of HierarchyId for EF Core functionality such as queries and updates requires the
Microsoft.EntityFrameworkCore.SqlServer.HierarchyId package. This package brings in
Microsoft.EntityFrameworkCore.SqlServer.Abstractions and Microsoft.SqlServer.Types
as transitive dependencies, and so is often the only package needed. Once the package
is installed, use of HierarchyId is enabled by calling UseHierarchyId as part of the
application's call to UseSqlServer . For example:
C#
options.UseSqlServer(
connectionString,
x => x.UseHierarchyId());
7 Note
Unofficial support for hierarchyid in EF Core has been available for many years via
the EntityFrameworkCore.SqlServer.HierarchyId package. This package has been
maintained as a collaboration between the community and the EF team. Now that
there is official support for hierarchyid in .NET, the code from this community
package forms, with the permission of the original contributors, the basis for the
official package described here. Many thanks to all those involved over the years,
including @aljones , @cutig3r , @huan086 , @kmataru ,
@mehdihaghshenas , and @vyrotek
Modeling hierarchies
The HierarchyId type can be used for properties of an entity type. For example, assume
we want to model the paternal family tree of some fictional halflings . In the entity
type for Halfling , a HierarchyId property can be used to locate each halfling in the
family tree.
C#
Tip
The code shown here and in the examples below comes from
HierarchyIdSample.cs .
Tip
In this tree:
The following code inserts this family tree into a database using EF Core:
C#
await AddRangeAsync(
new Halfling(HierarchyId.Parse("/"), "Balbo", 1167),
new Halfling(HierarchyId.Parse("/1/"), "Mungo", 1207),
new Halfling(HierarchyId.Parse("/2/"), "Pansy", 1212),
new Halfling(HierarchyId.Parse("/3/"), "Ponto", 1216),
new Halfling(HierarchyId.Parse("/4/"), "Largo", 1220),
new Halfling(HierarchyId.Parse("/5/"), "Lily", 1222),
new Halfling(HierarchyId.Parse("/1/1/"), "Bungo", 1246),
new Halfling(HierarchyId.Parse("/1/2/"), "Belba", 1256),
new Halfling(HierarchyId.Parse("/1/3/"), "Longo", 1260),
new Halfling(HierarchyId.Parse("/1/4/"), "Linda", 1262),
new Halfling(HierarchyId.Parse("/1/5/"), "Bingo", 1264),
new Halfling(HierarchyId.Parse("/3/1/"), "Rosa", 1256),
new Halfling(HierarchyId.Parse("/3/2/"), "Polo"),
new Halfling(HierarchyId.Parse("/4/1/"), "Fosco", 1264),
new Halfling(HierarchyId.Parse("/1/1/1/"), "Bilbo", 1290),
new Halfling(HierarchyId.Parse("/1/3/1/"), "Otho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/"), "Falco", 1303),
new Halfling(HierarchyId.Parse("/3/2/1/"), "Posco", 1302),
new Halfling(HierarchyId.Parse("/3/2/2/"), "Prisca", 1306),
new Halfling(HierarchyId.Parse("/4/1/1/"), "Dora", 1302),
new Halfling(HierarchyId.Parse("/4/1/2/"), "Drogo", 1308),
new Halfling(HierarchyId.Parse("/4/1/3/"), "Dudo", 1311),
new Halfling(HierarchyId.Parse("/1/3/1/1/"), "Lotho", 1310),
new Halfling(HierarchyId.Parse("/1/5/1/1/"), "Poppy", 1344),
new Halfling(HierarchyId.Parse("/3/2/1/1/"), "Ponto", 1346),
new Halfling(HierarchyId.Parse("/3/2/1/2/"), "Porto", 1348),
new Halfling(HierarchyId.Parse("/3/2/1/3/"), "Peony", 1350),
new Halfling(HierarchyId.Parse("/4/1/2/1/"), "Frodo", 1368),
new Halfling(HierarchyId.Parse("/4/1/3/1/"), "Daisy", 1350),
new Halfling(HierarchyId.Parse("/3/2/1/1/1/"), "Angelica", 1381));
await SaveChangesAsync();
Tip
If needed, decimal values can be used to create new nodes between two existing
nodes. For example, /3/2.5/2/ goes between /3/2/2/ and /3/3/2/ .
Querying hierarchies
HierarchyId exposes several methods that can be used in LINQ queries.
ノ Expand table
Method Description
In addition, the operators == , != , < , <= , > and >= can be used.
The following query uses GetLevel to return all halflings at a given level in the family
tree:
C#
SQL
Running this in a loop we can get the halflings for every generation:
text
Generation 0: Balbo
Generation 1: Mungo, Pansy, Ponto, Largo, Lily
Generation 2: Bungo, Belba, Longo, Linda, Bingo, Rosa, Polo, Fosco
Generation 3: Bilbo, Otho, Falco, Posco, Prisca, Dora, Drogo, Dudo
Generation 4: Lotho, Poppy, Ponto, Porto, Peony, Frodo, Daisy
Generation 5: Angelica
The following query uses GetAncestor to find the direct ancestor of a halfling, given that
halfling's name:
C#
SQL
The following query also uses GetAncestor , but this time to find the direct descendents
of a halfling, given that halfling's name:
C#
SQL
GetAncestor is useful for searching up or down a single level, or, indeed, a specified
number of levels. On the other hand, IsDescendantOf is useful for finding all ancestors
or dependents. For example, the following query uses IsDescendantOf to find the all the
ancestors of a halfling, given that halfling's name:
C#
.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor =>
ancestor.PathFromPatriarch.GetLevel());
) Important
IsDescendantOf returns true for itself, which is why it is filtered out in the query
above.
SQL
Running this query for the halfling "Bilbo" returns "Bungo", "Mungo", and "Balbo".
C#
SQL
Running this query for the halfling "Mungo" returns "Bungo", "Belba", "Longo", "Linda",
"Bingo", "Bilbo", "Otho", "Falco", "Lotho", and "Poppy".
One of the most common questions asked about this particular family tree is, "who is
the common ancestor of Frodo and Bilbo?" We can use IsDescendantOf to write such a
query:
C#
SQL
Running this query with "Bilbo" and "Frodo" tells us that their common ancestor is
"Balbo".
Updating hierarchies
The normal change tracking and SaveChanges mechanisms can be used to update
hierarchyid columns.
Re-parenting a sub-hierarchy
For example, I'm sure we all remember the scandal of SR 1752 (a.k.a. "LongoGate") when
DNA testing revealed that Longo was not in fact the son of Mungo, but actually the son
of Ponto! One fallout from this scandal was that the family tree needed to be re-written.
In particular, Longo and all his descendents needed to be re-parented from Mungo to
Ponto. GetReparentedValue can be used to do this. For example, first "Longo" and all his
descendents are queried:
C#
Then GetReparentedValue is used to update the HierarchyId for Longo and each
descendent, followed by a call to SaveChangesAsync :
C#
foreach (var descendent in longoAndDescendents)
{
descendent.PathFromPatriarch
= descendent.PathFromPatriarch.GetReparentedValue(
mungo.PathFromPatriarch, ponto.PathFromPatriarch)!;
}
await context.SaveChangesAsync();
SQL
text
@p1='9',
@p0='0x7BC0' (Nullable = false) (Size = 2) (DbType = Object),
@p3='16',
@p2='0x7BD6' (Nullable = false) (Size = 2) (DbType = Object),
@p5='23',
@p4='0x7BD6B0' (Nullable = false) (Size = 3) (DbType = Object)
7 Note
The parameters values for HierarchyId properties are sent to the database in their
compact, binary format.
Following the update, querying for the descendents of "Mungo" returns "Bungo",
"Belba", "Linda", "Bingo", "Bilbo", "Falco", and "Poppy", while querying for the
descendents of "Ponto" returns "Longo", "Rosa", "Polo", "Otho", "Posco", "Prisca",
"Lotho", "Ponto", "Porto", "Peony", and "Angelica".
Raw SQL queries for unmapped types
EF7 introduced raw SQL queries returning scalar types. This is enhanced in EF8 to
include raw SQL queries returning any mappable CLR type, without including that type
in the EF model.
Tip
Queries using unmapped types are executed using SqlQuery or SqlQueryRaw. The
former uses string interpolation to parameterize the query, which helps ensure that all
non-constant values are parameterized. For example, consider the following database
table:
SQL
SqlQuery can be used to query this table and return instances of a BlogPost type with
For example:
C#
For example:
C#
var start = new DateOnly(2022, 1, 1);
var end = new DateOnly(2023, 1, 1);
var postsIn2022 =
await context.Database
.SqlQuery<BlogPost>($"SELECT * FROM Posts as p WHERE p.PublishedOn
>= {start} AND p.PublishedOn < {end}")
.ToListAsync();
SQL
SELECT * FROM Posts as p WHERE p.PublishedOn >= @p0 AND p.PublishedOn < @p1
The type used for query results can contain common mapping constructs supported by
EF Core, such as parameterized constructors and mapping attributes. For example:
C#
[Column("Title")]
public string BlogTitle { get; set; }
7 Note
Types used in this way do not have keys defined and cannot have relationships to
other types. Types with relationships must be mapped in the model.
The type used must have a property for every value in the result set, but do not need to
match any table in the database. For example, the following type represents only a
subset of information for each post, and includes the blog name, which comes from the
Blogs table:
C#
C#
One nice feature of SqlQuery is that it returns an IQueryable which can be composed
on using LINQ. For example, a 'Where' clause can be added to the query above:
C#
var summariesIn2022 =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT b.Name AS BlogName, p.Title AS PostTitle,
p.PublishedOn
FROM Posts AS p
INNER JOIN Blogs AS b ON p.BlogId = b.Id")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
SQL
At this point it is worth remembering that all of the above can be done completely in
LINQ without the need to write any SQL. This includes returning instances of an
unmapped type like PostSummary . For example, the preceding query can be written in
LINQ as:
C#
var summaries =
await context.Posts.Select(
p => new PostSummary
{
BlogName = p.Blog.Name,
PostTitle = p.Title,
PublishedOn = p.PublishedOn,
})
.Where(p => p.PublishedOn >= start && p.PublishedOn < end)
.ToListAsync();
SQL
Tip
EF is able to generate cleaner SQL when it is responsible for the entire query than it
is when composing over user-supplied SQL because, in the former case, the full
semantics of the query is available to EF.
So far, all the queries have been executed directly against tables. SqlQuery can also be
used to return results from a view without mapping the view type in the EF model. For
example:
C#
var summariesFromView =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM PostAndBlogSummariesView")
.Where(p => p.PublishedOn >= cutoffDate && p.PublishedOn < end)
.ToListAsync();
C#
var summariesFromFunc =
await context.Database.SqlQuery<PostSummary>(
@$"SELECT * FROM GetPostsPublishedAfter({cutoffDate})")
.Where(p => p.PublishedOn < end)
.ToListAsync();
The returned IQueryable can be composed upon when it is the result of a view or
function, just like it can be for the result of a table query. Stored procedures can be also
be executed using SqlQuery , but most databases do not support composing over them.
For example:
C#
var summariesFromStoredProc =
await context.Database.SqlQuery<PostSummary>(
@$"exec GetRecentPostSummariesProc")
.ToListAsync();
Enhancements to lazy-loading
Tip
The code for the lazy-loading examples shown below comes from
LazyLoadingSample.cs .
C#
Console.WriteLine();
Console.Write("Choose a blog: ");
if (int.TryParse(ReadLine(), out var blogId))
{
Console.WriteLine("Posts:");
foreach (var post in blogs[blogId - 1].Posts)
{
Console.WriteLine($" {post.Title}");
}
}
EF8 also reports whether or not a given navigation is loaded for entities not tracked by
the context. For example:
C#
There are a few important considerations when using lazy-loading in this way:
Lazy-loading will only succeed until the DbContext used to query the entity is
disposed.
Entities queried in this way maintain a reference to their DbContext , even though
they are not tracked by it. Care should be taken to avoid memory leaks if the entity
instances will have long lifetimes.
Explicitly detaching the entity by setting its state to EntityState.Detached severs
the reference to the DbContext and lazy-loading will no longer work.
Remember that all lazy-loading uses synchronous I/O, since there is no way to
access a property in an asynchronous manner.
Lazy-loading from untracked entities works for both lazy-loading proxies and lazy-
loading without proxies.
C#
C#
modelBuilder
.Entity<Post>()
.Navigation(p => p.Author)
.EnableLazyLoading(false);
Disabling Lazy-loading like this works for both lazy-loading proxies and lazy-loading
without proxies.
This can be changed in EF8 to opt-in to the classic EF6 behavior such that a navigation
can be made to not lazy-load simply by making the navigation non-virtual. This opt-in is
configured as part of the call to UseLazyLoadingProxies . For example:
C#
EF8 contains new public APIs so that applications can now use these data structures to
efficiently lookup tracked entities. These APIs are accessed through the
LocalView<TEntity> of the entity type. For example, to lookup a tracked entity by its
primary key:
C#
Tip
The FindEntry method returns either the EntityEntry<TEntity> for the tracked entity, or
null if no entity with the given key is being tracked. Like all methods on LocalView , the
database is never queried, even if the entity is not found. The returned entry contains
the entity itself, as well as tracking information. For example:
C#
Looking up an entity by anything other than a primary key requires that the property
name be specified. For example, to look up by an alternate key:
C#
So far, the lookups have always returned a single entry, or null . However, some lookups
can return more than one entry, such as when looking up by a non-unique foreign key.
The GetEntries method should be used for these lookups. For example:
C#
In all these cases, the value being used for the lookup is either a primary key, alternate
key, or foreign key value. EF uses its internal data structures for these lookups. However,
lookups by value can also be used for the value of any property or combination of
properties. For example, to find all archived posts:
C#
var archivedPostEntries =
context.Posts.Local.GetEntries(nameof(Post.Archived), true);
This lookup requires a scan of all tracked Post instances, and so will be less efficient
than key lookups. However, it is usually still faster than naive queries using
ChangeTracker.Entries<TEntity>().
C#
Model building
C#
With the convention of using the class names for discriminator values, the possible
values here are "PaperbackEdition", "HardbackEdition", and "Magazine", and hence the
discriminator column is configured for a max length of 21. For example, when using SQL
Server:
SQL
Tip
Fibonacci numbers are used to limit the number of times a migration is generated
to change the column length as new types are added to the hierarchy.
DateOnly/TimeOnly supported on SQL Server
The DateOnly and TimeOnly types were introduced in .NET 6 and have been supported
for several database providers (e.g. SQLite, MySQL, and PostgreSQL) since their
introduction. For SQL Server, the recent release of a Microsoft.Data.SqlClient package
targeting .NET 6 has allowed ErikEJ to add support for these types at the ADO.NET
level . This in turn paved the way for support in EF8 for DateOnly and TimeOnly as
properties in entity types.
Tip
C#
[Owned]
public class OpeningHours
{
public OpeningHours(DayOfWeek dayOfWeek, TimeOnly? opensAt, TimeOnly?
closesAt)
{
DayOfWeek = dayOfWeek;
OpensAt = opensAt;
ClosesAt = closesAt;
}
public DayOfWeek DayOfWeek { get; private set; }
public TimeOnly? OpensAt { get; set; }
public TimeOnly? ClosesAt { get; set; }
}
Tip
7 Note
This model represents only British schools and stores times as local (GMT) times.
Handling different timezones would complicate this code significantly. Note that
using DateTimeOffset would not help here, since opening and closing times have
different offsets depending whether daylight saving time is active or not.
These entity types map to the following tables when using SQL Server. Notice that the
DateOnly properties map to date columns, and the TimeOnly properties map to time
columns.
SQL
Queries using DateOnly and TimeOnly work in the expected manner. For example, the
following LINQ query finds schools that are currently open:
C#
SQL
DateOnly and TimeOnly can also be used in JSON columns. For example, OpeningHours
can be saved as a JSON document, resulting in data that looks like this:
ノ Expand table
Column Value
Id 2
Founded 1964-05-01
OpeningHours
[
{ "DayOfWeek": "Sunday", "ClosesAt": null, "OpensAt": null
},
{ "DayOfWeek": "Monday", "ClosesAt": "15:35:00",
"OpensAt": "08:45:00" },
{ "DayOfWeek": "Tuesday", "ClosesAt": "15:35:00",
"OpensAt": "08:45:00" },
{ "DayOfWeek": "Wednesday", "ClosesAt": "15:35:00",
"OpensAt": "08:45:00" },
{ "DayOfWeek": "Thursday", "ClosesAt": "15:35:00",
"OpensAt": "08:45:00" },
{ "DayOfWeek": "Friday", "ClosesAt": "12:50:00",
"OpensAt": "08:45:00" },
{ "DayOfWeek": "Saturday", "ClosesAt": null, "OpensAt":
null }
]
Combining two features from EF8, we can now query for opening hours by indexing into
the JSON collection. For example:
C#
SQL
Finally, updates and deletes can be accomplished with tracking and SaveChanges, or
using ExecuteUpdate/ExecuteDelete. For example:
C#
await context.Schools
.Where(e => e.Terms.Any(t => t.LastDay.Year == 2022))
.SelectMany(e => e.Terms)
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LastDay, t =>
t.LastDay.AddDays(1)));
SQL
UPDATE [t0]
SET [t0].[LastDay] = DATEADD(day, CAST(1 AS int), [t0].[LastDay])
FROM [Schools] AS [s]
INNER JOIN [Term] AS [t0] ON [s].[Id] = [t0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND DATEPART(year, [t].[LastDay]) =
2022)
2 Warning
These database systems have differences from normal SQL Server and Azure SQL
databases. These differences mean that not all EF Core functionality is supported
when writing queries against or performing other operations with these database
systems.
EF Core 8 translates calls to these generic math APIs in LINQ using providers' existing
SQL translations for Math and MathF . This means you're now free to choose between
calls either like Math.Sin or double.Sin in your EF queries.
We worked with the .NET team to add two new generic math methods in .NET 8 that are
implemented on double and float . These are also translated to SQL in EF Core 8.
ノ Expand table
.NET SQL
DegreesToRadians RADIANS
RadiansToDegrees DEGREES
Finally, we worked with Eric Sink in the SQLitePCLRaw project to enable the SQLite
math functions in their builds of the native SQLite library. This includes the native
library you get by default when you install the EF Core SQLite provider. This enables
several new SQL translations in LINQ including: Acos, Acosh, Asin, Asinh, Atan, Atan2,
Atanh, Ceiling, Cos, Cosh, DegreesToRadians, Exp, Floor, Log, Log2, Log10, Pow,
RadiansToDegrees, Sign, Sin, Sinh, Sqrt, Tan, Tanh, and Truncate.
.NET CLI
SQLite provider by converting between them and one of the four primitive SQLite types.
In EF Core 8, we now use the data format and column type name in addition to the
SQLite type in order to determine a more appropriate .NET type to use in the model.
The following tables show some of the cases where the additional information leads to
better property types in the model.
ノ Expand table
BIGINT long
ノ Expand table
C#
C#
Tip
In order for EF to make use of this, it must determine when and when not to send a
value for the column. By default, EF uses the CLR default as a sentinel for this. That is,
when the value of Status or LeaseDate in the examples above are the CLR defaults for
these types, then EF interprets that to mean that the property has not been set, and so
does not send a value to the database. This works well for reference types--for example,
if the string property Status is null , then EF doesn't send null to the database, but
rather does not include any value so that the database default ( "Hidden" ) is used.
Likewise, for the DateTime property LeaseDate , EF will not insert the CLR default value of
1/1/0001 12:00:00 AM , but will instead omit this value so that database default is used.
However, in some cases the CLR default value is a valid value to insert. EF8 handles this
by allowing the sentinel value for a column to change. For example, consider an integer
column configured with a database default:
C#
In this case, we want the new entity to be inserted with the given number of credits,
unless this is not specified, in which case 10 credits are assigned. However, this means
that inserting a record with zero credits is not possible, since zero is the CLR default, and
hence will cause EF to send no value. In EF8, this can be fixed by changing the sentinel
for the property from zero to -1 :
C#
EF will now only use the database default if Credits is set to -1 ; a value of zero will be
inserted like any other amount.
It can often be useful to reflect this in the entity type as well as in the EF configuration.
For example:
C#
This means that the sentinel value of -1 gets set automatically when the instance is
created, meaning that the property starts in its "not-set" state.
Tip
If you want to configure the database default constraint for use when Migrations
creates the column, but you want EF to always insert a value, then configure the
property as not-generated. For example, b.Property(e =>
e.Credits).HasDefaultValueSql(10).ValueGeneratedNever(); .
On the other hand, if the database default value is true , this means when the property
value is false , then the database default will be used, which is true ! And when the
property value is true , then true will be inserted. So, the value in the column will
always end true in the database, regardless of what the property value is.
EF8 fixes this problem by setting the sentinel for bool properties to the same value as
the database default value. Both cases above then result in the correct value being
inserted, regardless of whether the database default is true or false .
Tip
When scaffolding from an existing database, EF8 parses and then includes simple
default values into HasDefaultValue calls. (Previously, all default values were
scaffolded as opaque HasDefaultValueSql calls.) This means that non-nullable bool
columns with a true or false constant database default are no longer scaffolded
as nullable.
C#
C#
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate);
With this configuration, EF will exclude sending the value to the database when it is set
to Level.Beginner , and instead Level.Intermediate is assigned by the database. This
isn't what was intended!
The problem would not have occurred if the the enum been defined with the "unknown"
or "unspecified" value being the database default:
C#
However, it is not always possible to change an existing enum, so in EF8, the sentinel
can again be specified. For example, going back to the original enum:
C#
modelBuilder.Entity<Course>()
.Property(e => e.Level)
.HasDefaultValue(Level.Intermediate)
.HasSentinel(Level.Unspecified);
Now Level.Beginner will be inserted as normal, and the database default will only be
used when the property value is Level.Unspecified . It can again be useful to reflect this
in the entity type itself. For example:
C#
C#
The backing field here will remain null unless the property setter is actually called. That
is, the value of the backing field is a better indication of whether the property has been
set or not than the CLR default of the property. This works out-of-the box with EF, since
EF will use the backing field to read and write the property by default.
However, in EF7, ExecuteUpdate and ExecuteDelete did not support updates accessing
multiple entity types even when the query ultimately affected a single table. EF8 removes
this limitation. For example, consider a Customer entity type with CustomerInfo owned
type:
C#
[Owned]
public class CustomerInfo
{
public string? Tag { get; set; }
}
Both of these entity types map to the Customers table. However, the following bulk
update fails on EF7 because it uses both entity types:
C#
await context.Customers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(
s => s.SetProperty(b => b.CustomerInfo.Tag, "Tagged")
.SetProperty(b => b.Name, b => b.Name + "_Tagged"));
In EF8, this now translates to the following SQL when using Azure SQL:
SQL
UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
[c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0
Similarly, instances returned from a Union query can be updated as long as the updates
all target the same table. For example, we can update any Customer with a region of
France , and at the same time, any Customer who has visited a store with the region
France :
C#
await context.CustomersWithStores
.Where(e => e.Region == "France")
.Union(context.Stores.Where(e => e.Region == "France").SelectMany(e =>
e.Customers))
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Tag, "The French
Connection"));
In EF8, this query generates the following when using Azure SQL:
SQL
UPDATE [c]
SET [c].[Tag] = N'The French Connection'
FROM [CustomersWithStores] AS [c]
INNER JOIN (
SELECT [c0].[Id], [c0].[Name], [c0].[Region], [c0].[StoreId], [c0].[Tag]
FROM [CustomersWithStores] AS [c0]
WHERE [c0].[Region] = N'France'
UNION
SELECT [c1].[Id], [c1].[Name], [c1].[Region], [c1].[StoreId], [c1].[Tag]
FROM [Stores] AS [s]
INNER JOIN [CustomersWithStores] AS [c1] ON [s].[Id] = [c1].[StoreId]
WHERE [s].[Region] = N'France'
) AS [t] ON [c].[Id] = [t].[Id]
C#
[Table("TptSpecialCustomers")]
public class SpecialCustomerTpt : CustomerTpt
{
public string? Note { get; set; }
}
[Table("TptCustomers")]
public class CustomerTpt
{
public int Id { get; set; }
public required string Name { get; set; }
}
C#
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));
C#
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Name, b => b.Name + "
(Noted)"));
However, EF8 fails attempting to update both the Name and the Note properties because
they are mapped to different tables. For example:
C#
await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted")
.SetProperty(b => b.Name, b => b.Name + " (Noted)"));
text
C#
SQL
SELECT b."Id", b."Name"
FROM "Blogs" AS b
WHERE EXISTS (
SELECT 1
FROM "Posts" AS p
WHERE p."BlogId" = b."Id")
Since the subquery references the external Blogs table (via b."Id" ), this is a correlated
subquery, meaning that the Posts subquery must be executed for each row in the Blogs
table. In EF8, the following SQL is generated instead:
SQL
Since the subquery no longer references Blogs , it can be evaluated once, yielding
massive performance improvements on most database systems. However, some
database systems, most notably SQL Server, the database is able to optimize the first
query to the second query so that the performance is the same.
default, SqlClient exposes rowversion types as byte[] , despite mutable reference types
being a bad match for rowversion semantics. In EF8, it is easy to instead map
rowversion columns to long or ulong properties. For example:
C#
modelBuilder.Entity<Blog>()
.Property(e => e.RowVersion)
.IsRowVersion();
Parentheses elimination
Generating readable SQL is an important goal for EF Core. In EF8, the generated SQL is
more readable through automatic elimination of unneeded parenthesis. For example,
the following LINQ query:
C#
await ctx.Customers
.Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName !=
null)
.ToListAsync();
SQL
SQL
For example, to opt-out of OUTPUT when using the SQL Server/Azure SQL provider:
C#
modelBuilder.Entity<Customer>().ToTable(tb =>
tb.UseSqlOutputClause(false));
modelBuilder.Entity<Customer>().ToTable(tb =>
tb.UseSqlReturningClause(false));