0% found this document useful (0 votes)
504 views253 pages

Dotnet Whats New

qww

Uploaded by

Ana Grossmuller
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
504 views253 pages

Dotnet Whats New

qww

Uploaded by

Ana Grossmuller
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 253

Tell us about your PDF experience.

.NET - what's new?


Welcome to what's new in .NET and .NET docs. Use this page to navigate to different
articles that cover new features for a variety of areas.

.NET 9 release updates

h WHAT'S NEW

.NET 9

ASP.NET Core 9.0

.NET MAUI 9

EF Core 9

.NET 8 release updates

h WHAT'S NEW

.NET 8

ASP.NET Core 8.0

.NET MAUI 8

EF Core 8

Windows Forms (.NET 8)

WPF (.NET 8)

Latest documentation updates

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

What's new for Visual Basic

Contribute to docs

e OVERVIEW

.NET docs repository

Project structure and labels for issues and pull requests

.NET Foundation

p CONCEPT

Microsoft docs contributor guide

.NET docs contributor guide

.NET developers

a DOWNLOAD

Download the .NET SDK

s SAMPLE

.NET samples browser

b GET STARTED

.NET on Q&A

.NET tech community forums


h WHAT'S NEW

Community

Release notes

h WHAT'S NEW

.NET

ASP.NET Core 9.0

Visual Studio 2022

Visual Studio Code


.NET docs: What's new for October 2024
Article • 11/02/2024

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.

.NET breaking changes

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

Azure SDK for .NET

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.

shethaadit - Adit Sheth Merged Pull Requests 3

BartoszKlonowski - Bartosz Klonowski Merged Pull Requests 2

bigboybamo - Olabamiji Oyetubo Merged Pull Requests 2

juner - juner Merged Pull Requests 2

Marusyk - Roman Marusyk Merged Pull Requests 2

timdeschryver - Tim Deschryver Merged Pull Requests 2

aarijimam - Nawab Aarij Imam Merged Pull Requests 1


alexravenna - Alex Ravenna Merged Pull Requests 1

am11 - Adeel Mujahid Merged Pull Requests 1

ardalis - Steve Smith Merged Pull Requests 1

azarboon - Mahdi Azarboon Merged Pull Requests 1

batkaevruslan - Ruslan Batkaev Merged Pull Requests 1

BigT-88 - Merged Pull Requests 1

colejohnson66 - Cole Tobin Merged Pull Requests 1

fabrizziocht - Fabrizzio Chavez Merged Pull Requests 1

gbamqzkdyg - Luca Ma Merged Pull Requests 1

glen-84 - Glen Merged Pull Requests 1

hakenr - Robert Haken Merged Pull Requests 1

HugoRoss - Christoph Hafner Merged Pull Requests 1

ichensky - Ivan Chensky Merged Pull Requests 1

janus-toendering - Janus Tøndering Merged Pull Requests 1

jochenkirstaetter - Jochen Kirstätter Merged Pull Requests 1

jsedlak - John Sedlak Merged Pull Requests 1

magiudev - Miguel Angel Echeverri Quiroz Merged Pull Requests 1

MarGraz - Merged Pull Requests 1

Navis304 - Robert Merged Pull Requests 1

omajid - Omair Majid Merged Pull Requests 1

PawelAdamczuk - Paweł Adamczuk Merged Pull Requests 1

pragnya17 - Pragnya Merged Pull Requests 1

Rageking8 - Merged Pull Requests 1

samwherever - Sam Allen Merged Pull Requests 1

Swimburger - Niels Swimberghe Merged Pull Requests 1

vcrobe - Merged Pull Requests 1

WeihanLi - Weihan Li Merged Pull Requests 1

xakep139 - Nikita Balabaev Merged Pull Requests 1

xtqqczze - Merged Pull Requests 1

Youssef1313 - Youssef Victor Merged Pull Requests 1


.NET docs: What's new for September
2024
Article • 10/02/2024

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.

.NET breaking changes

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.

Azure SDK for .NET

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.

BartoszKlonowski - Bartosz Klonowski Merged Pull Requests 3

hakenr - Robert Haken Merged Pull Requests 3

mpostol - Mariusz Postol Merged Pull Requests 2

Roshni-Gandhi - Merged Pull Requests 2

AptiviCEO - Aptivi CEO Merged Pull Requests 1

bb-froggy - Christoph Hannebauer Merged Pull Requests 1

bravequickcleverfibreyarn - boldswiftsmartfiberhank Merged Pull Requests 1

ChinoUkaegbu - Merged Pull Requests 1

colejohnson66 - Cole Tobin Merged Pull Requests 1

da1910 - Doug Addy Merged Pull Requests 1

geniuszxy - Shingo Merged Pull Requests 1

Haidar0096 - Haidar Mehsen Merged Pull Requests 1

HEJOK254 - Jakub Dębski Merged Pull Requests 1

jairbubbles - Julien Richard Merged Pull Requests 1


koenigst - Merged Pull Requests 1

Legend4it - Ali Abdulhussein Merged Pull Requests 1

luizfls - Luiz Felipe Silva Merged Pull Requests 1

melbasiouny - Mostafa Merged Pull Requests 1

normandev92 - Merged Pull Requests 1

ousiax - Jon X Merged Pull Requests 1

sammychinedu2ky - Samson Amaugo Merged Pull Requests 1

samwherever - Sam Allen Merged Pull Requests 1

Smaug123 - Patrick Stevens Merged Pull Requests 1

stashut - Stanislav Hut Merged Pull Requests 1

timdeschryver - Tim Deschryver Merged Pull Requests 1

udidahan - Udi Dahan Merged Pull Requests 1

xlxdxy - Merged Pull Requests 1

xparadoxical - Merged Pull Requests 1


.NET docs: What's new for August 2024
Article • 09/04/2024

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.

.NET breaking changes

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)

Azure SDK for .NET

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.

BartoszKlonowski - Bartosz Klonowski Merged Pull Requests 4

samwherever - Sam Allen Merged Pull Requests 4

Rageking8 - Merged Pull Requests 2

udidahan - Udi Dahan Merged Pull Requests 2

8chan-co - Merged Pull Requests 1

alkampfergit - Gian Maria Merged Pull Requests 1

baonguyen2310 - Bao Nguyen Chi Merged Pull Requests 1

bigboybamo - Olabamiji Oyetubo Merged Pull Requests 1

boggye - Merged Pull Requests 1

ChinoUkaegbu - Merged Pull Requests 1

daverayment - Dave Rayment Merged Pull Requests 1

dkroderos - David King Roderos Merged Pull Requests 1

GeRRy1337 - Merged Pull Requests 1

jbrekle - Jonas Brekle Merged Pull Requests 1

Progman2002 - Merged Pull Requests 1

rmunn - Robin Munn Merged Pull Requests 1


samibinsami - Saad Bin Sami Merged Pull Requests 1

warrenZY - Xiao.ZY Merged Pull Requests 1

xtqqczze - Merged Pull Requests 1

YoshiRulz - James Groom Merged Pull Requests 1


What's new in .NET 9
Article • 11/05/2024

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.

The runtime also includes numerous performance improvements, including loop


optimizations, inlining, and Arm64 vectorization and code generation.

For more information, see What's new in the .NET 9 runtime.

.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 collection types, the System.Collections.Generic.PriorityQueue<TElement,TPriority>


type includes a new Remove(TElement, TElement, TPriority,
IEqualityComparer<TElement>) method that you can use to update the priority of an
item in the queue.
For cryptography, .NET 9 adds a new one-shot hash method on the
CryptographicOperations type. It also adds new classes that use the KMAC algorithm.

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.

For more information, see What's new in the .NET 9 libraries.

.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

New lock type and semantics


New escape sequence - \e
Method group natural type improvements
Implicit indexer access in object initializers
Enable ref locals and unsafe contexts in iterators and async methods
Enable ref struct types to implement interfaces
Allow ref struct types as arguments for type parameters in generics.
Partial properties and indexers are now allowed in partial types.
Overload resolution priority allows library authors to designate one overload as
better than others.

For more information, see What's new in C# 13.

F# 9
F# 9 ships with the .NET 9 SDK and includes the following new features:

Nullable reference types


Discriminated union .Is* properties
Partial active patterns can return bool instead of unit option
Prefer extension methods to intrinsic properties when arguments are provided
Empty-bodied computation expressions
Hash directives are allowed to take non-string arguments
Extended #help directive in fsi to show documentation in the read-eval-print loop
(REPL)
Allow #nowarn to support the FS prefix on error codes to disable warnings
Warning about TailCall attribute on non-recursive functions or let-bound values
Enforce attribute targets
Random functions for collections
C# collection expression support for F# lists and sets
Various developer productivity, performance and tooling improvements

For more information, see What's new in F# 9.

Windows Presentation Foundation


Windows Presentation Foundation (WPF) includes support for Windows 11 theming and
hyphen-based ligatures. For more information, see WPF in .NET 9 Preview 4 - Release
Notes .

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.

Static asset delivery optimization


MapStaticAssets routing endpoint conventions is a new feature that optimizes the
delivery of static assets in ASP.NET Core apps.

For information on static asset delivery for Blazor apps, see ASP.NET Core Blazor static
files.

Following production best practices for serving static assets requires a significant
amount of work and technical expertise. Without optimizations like compression,
caching, and fingerprints :

The browser has to make additional requests on every page load.


More bytes than necessary are transferred through the network.
Sometimes stale versions of files are served to clients.

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();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

+app.MapStaticAssets();
-app.UseStaticFiles();
app.MapRazorPages();

app.Run();

MapStaticAssets operates by combining build and publish-time processes to collect

information about all the static resources in an app. This information is then utilized by
the runtime library to efficiently serve these files to the browser.

MapStaticAssets can replace UseStaticFiles in most situations, however, it's optimized

for serving the assets that the app has knowledge of at build and publish time. If the
app serves assets from other locations, such as disk or embedded resources,
UseStaticFiles should be used.

MapStaticAssets provides the following benefits not found with UseStaticFiles :

Build time compression for all the assets in the app:


gzip during development and gzip + brotli during publish.

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

File Original Compressed % Reduction

bootstrap.min.css 163 17.5 89.26%

jquery.js 89.6 28 68.75%

bootstrap.min.js 78.5 20 74.52%

Total 331.1 65.5 80.20%

The following table shows the original and compressed sizes using the Fluent UI Blazor
components library :

ノ Expand table

File Original Compressed % Reduction

fluent.js 384 73 80.99%

fluent.css 94 11 88.30%

Total 478 84 82.43%

For a total of 478 KB uncompressed to 84 KB compressed.

The following table shows the original and compressed sizes using the MudBlazor
Blazor components library:

ノ Expand table

File Original Compressed Reduction

MudBlazor.min.css 541 37.5 93.07%

MudBlazor.min.js 47.4 9.2 80.59%

Total 588.4 46.7 92.07%

Optimization happens automatically when using MapStaticAssets . When a library is


added or updated, for example with new JavaScript or CSS, the assets are optimized as
part of the build. Optimization is especially beneficial to mobile environments that can
have a lower bandwidth or an unreliable connections.

For more information on the new file delivery features, see the following resources:

Static files in ASP.NET Core


ASP.NET Core Blazor static files

Enabling dynamic compression on the server vs using


MapStaticAssets

MapStaticAssets has the following advantages over dynamic compression on the server:

Is simpler because there is no server specific configuration.


Is more performant because the assets are compressed at build time.
Allows the developer to spend extra time during the build process to ensure that
the assets are the minimum size.

Consider the following table comparing MudBlazor compression with IIS dynamic
compression and MapStaticAssets :

ノ Expand table

IIS gzip MapStaticAssets MapStaticAssets reduction

≅ 90 37.5 59%

Blazor
This section describes new features for Blazor.

.NET MAUI Blazor Hybrid and Web App solution template


A new solution template makes it easier to create .NET MAUI native and Blazor web
client apps that share the same UI. This template shows how to create client apps that
maximize code reuse and target Android, iOS, Mac, Windows, and Web.

Key features of this template include:

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

dotnet workload install maui

Create a solution from the project template in a command shell using the following
command:

.NET CLI

dotnet new maui-blazor-web

The template is also available in Visual Studio.

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.

Detect rendering location, interactivity, and assigned


render mode at runtime
We've introduced a new API designed to simplify the process of querying component
states at runtime. This API provides the following capabilities:

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.

For more information, see ASP.NET Core Blazor render modes.


Improved server-side reconnection experience:
The following enhancements have been made to the default server-side reconnection
experience:

When the user navigates back to an app with a disconnected circuit, reconnection
is attempted immediately rather than waiting for the duration of the next
reconnect interval. This improves the user experience when navigating to an app in
a browser tab that has gone to sleep.

When a reconnection attempt reaches the server but the server has already
released the circuit, a page refresh occurs automatically. This prevents the user
from having to manually refresh the page if it's likely going to result in a successful
reconnection.

Reconnect timing uses a computed backoff strategy. By default, the first several
reconnection attempts occur in rapid succession without a retry interval before
computed delays are introduced between attempts. You can customize the retry
interval behavior by specifying a function to compute the retry interval, as the
following exponential backoff example demonstrates:

JavaScript

Blazor.start({
circuit: {
reconnectionOptions: {
retryIntervalMilliseconds: (previousAttempts, maxRetries) =>
previousAttempts >= maxRetries ? null : previousAttempts * 1000
},
},
});

The styling of the default reconnect UI has been modernized.

For more information, see ASP.NET Core Blazor SignalR guidance.

Simplified authentication state serialization for Blazor


Web Apps
New APIs make it easier to add authentication to an existing Blazor Web App. When you
create a new Blazor Web App with authentication using Individual Accounts and you
enable WebAssembly-based interactivity, the project includes a custom
AuthenticationStateProvider in both the server and client projects.
These providers flow the user's authentication state to the browser. Authenticating on
the server rather than the client allows the app to access authentication state during
prerendering and before the .NET WebAssembly runtime is initialized.

The custom AuthenticationStateProvider implementations use the Persistent Component


State service (PersistentComponentState) to serialize the authentication state into HTML
comments and read it back from WebAssembly to create a new AuthenticationState
instance.

This works well if you've started from the Blazor Web App project template and selected
the Individual Accounts option, but it's a lot of code to implement yourself or copy if
you're trying to add authentication to an existing project. There are now APIs, which are
now part of the Blazor Web App project template, that can be called in the server and
client projects to add this functionality:

AddAuthenticationStateSerialization: Adds the necessary services to serialize the


authentication state on the server.
AddAuthenticationStateDeserialization: Adds the necessary services to deserialize
the authentication state in the browser.

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:

Blazor Identity UI (Individual Accounts)


Manage authentication state in Blazor Web Apps

Add static server-side rendering (SSR) pages to a


globally-interactive Blazor Web App
With the release of .NET 9, it's now simpler to add static SSR pages to apps that adopt
global interactivity.

This approach is only useful when the app has specific pages that can't work with
interactive Server or WebAssembly rendering. For example, adopt this approach for
pages that depend on reading/writing HTTP cookies and can only work in a
request/response cycle instead of interactive rendering. For pages that work with
interactive rendering, you shouldn't force them to use static SSR rendering, as it's less
efficient and less responsive for the end user.
Mark any Razor component page with the new [ExcludeFromInteractiveRouting]
attribute assigned with the @attribute Razor directive:

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.

In the App component, use the pattern in the following example:

Pages that aren't annotated with the [ExcludeFromInteractiveRouting] attribute


default to the InteractiveServer render mode with global interactivity. You can
replace InteractiveServer with InteractiveWebAssembly or InteractiveAuto to
specify a different default global render mode.
Pages annotated with the [ExcludeFromInteractiveRouting] attribute adopt static
SSR ( PageRenderMode is assigned null ).

razor

<!DOCTYPE html>
<html>
<head>
...
<HeadOutlet @rendermode="@PageRenderMode" />
</head>
<body>
<Routes @rendermode="@PageRenderMode" />
...
</body>
</html>

@code {
[CascadingParameter]
private HttpContext HttpContext { get; set; } = default!;

private IComponentRenderMode? PageRenderMode


=> HttpContext.AcceptsInteractiveRouting() ? InteractiveServer :
null;
}

An alternative to using the


RazorComponentsEndpointHttpContextExtensions.AcceptsInteractiveRouting extension
method is to read endpoint metadata manually using
HttpContext.GetEndpoint()?.Metadata .

This feature is covered by the reference documentation in ASP.NET Core Blazor render
modes.

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#

public partial class ConstructorInjection(NavigationManager navigation)


{
private void HandleClick()
{
navigation.NavigateTo("/counter");
}
}

For more information, see ASP.NET Core Blazor dependency injection.

Websocket compression for Interactive Server


components
By default, Interactive Server components enable compression for WebSocket
connections and set a frame-ancestors Content Security Policy (CSP) directive set to
'self' , which only permits embedding the app in an <iframe> of the origin from which

the app is served when compression is enabled or when a configuration for the
WebSocket context is provided.

Compression can be disabled by setting ConfigureWebSocketOptions to null , which


reduces the vulnerability of the app to attack but may result in reduced performance:

C#
.AddInteractiveServerRenderMode(o => o.ConfigureWebSocketOptions = null)

Configure a stricter frame-ancestors CSP with a value of 'none' (single quotes


required), which allows WebSocket compression but prevents browsers from embedding
the app into any <iframe> :

C#

.AddInteractiveServerRenderMode(o => o.ContentSecurityFrameAncestorsPolicy =


"'none'")

For more information, see the following resources:

ASP.NET Core Blazor SignalR guidance


Threat mitigation guidance for ASP.NET Core Blazor interactive server-side
rendering

Handle keyboard composition events in Blazor


The new KeyboardEventArgs.IsComposing property indicates if the keyboard event is part
of a composition session . Tracking the composition state of keyboard events is crucial
for handling international character input methods.

Added OverscanCount parameter to QuickGrid


The QuickGrid component now exposes an OverscanCount property that specifies how
many additional rows are rendered before and after the visible region when
virtualization is enabled.

The default OverscanCount is 3. The following example increases the OverscanCount to 4:

razor

<QuickGrid ItemsProvider="itemsProvider" Virtualize="true"


OverscanCount="4">
...
</QuickGrid>

InputNumber component supports the type="range"


attribute
The InputNumber<TValue> component now supports the type="range" attribute ,
which creates a range input that supports model binding and form validation, typically
rendered as a slider or dial control rather than a text box:

razor

<EditForm Model="Model" OnSubmit="Submit" FormName="EngineForm">


<div>
<label>
Nacelle Count (2-6):
<InputNumber @bind-Value="Model!.NacelleCount" max="6" min="2"
step="1" type="range" />
</label>
</div>
<div>
<button type="submit">Submit</button>
</div>
</EditForm>

@code {
[SupplyParameterFromForm]
private EngineSpecifications? Model { get; set; }

protected override void OnInitialized() => Model ??= new();

private void Submit() {}

public class EngineSpecifications


{
[Required, Range(minimum: 2, maximum: 6)]
public int NacelleCount { get; set; }
}
}

SignalR
This section describes new features for SignalR.

Polymorphic type support in SignalR Hubs


Hub methods can now accept a base class instead of the derived class to enable
polymorphic scenarios. The base type needs to be annotated to allow polymorphism.

C#

public class MyHub : Hub


{
public void Method(JsonPerson person)
{
if (person is JsonPersonExtended)
{
}
else if (person is JsonPersonExtended2)
{
}
else
{
}
}
}

[JsonPolymorphic]
[JsonDerivedType(typeof(JsonPersonExtended), nameof(JsonPersonExtended))]
[JsonDerivedType(typeof(JsonPersonExtended2), nameof(JsonPersonExtended2))]
private class JsonPerson
{
public string Name { get; set; }
public Person Child { get; set; }
public Person Parent { get; set; }
}

private class JsonPersonExtended : JsonPerson


{
public int Age { get; set; }
}

private class JsonPersonExtended2 : JsonPerson


{
public string Location { get; set; }
}

Improved Activities for SignalR


SignalR now has an ActivitySource named Microsoft.AspNetCore.SignalR.Server that
emits events for hub method calls:

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" />

Add the following startup code to the Program.cs file:

C#

// Set OTEL_EXPORTER_OTLP_ENDPOINT environment variable depending on where


your OTEL endpoint is
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddSignalR();

builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
if (builder.Environment.IsDevelopment())
{
// 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());

The following is example output from the Aspire Dashboard:

SignalR supports trimming and Native AOT


Continuing the Native AOT journey started in .NET 8, we have enabled trimming and
native ahead-of-time (AOT) compilation support for both SignalR client and server
scenarios. You can now take advantage of the performance benefits of using Native AOT
in applications that use SignalR for real-time web communications.

Getting started

Install the latest .NET 9 SDK .

Create a solution from the webapiaot template in a command shell using the following
command:

.NET CLI

dotnet new webapiaot -o SignalRChatAOTExample

Replace the contents of the Program.cs file with the following SignalR code:

C#

using Microsoft.AspNetCore.SignalR;
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.AddSignalR();
builder.Services.Configure<JsonHubProtocolOptions>(o =>
{
o.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});

var app = builder.Build();

app.MapHub<ChatHub>("/chatHub");
app.MapGet("/", () => Results.Content("""
<!DOCTYPE html>
<html>
<head>
<title>SignalR Chat</title>
</head>
<body>
<input id="userInput" placeholder="Enter your name" />
<input id="messageInput" placeholder="Type a message" />
<button onclick="sendMessage()">Send</button>
<ul id="messages"></ul>

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-
signalr/8.0.7/signalr.min.js"></script>
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();

connection.on("ReceiveMessage", (user, message) => {


const li = document.createElement("li");
li.textContent = `${user}: ${message}`;
document.getElementById("messages").appendChild(li);
});

async function sendMessage() {


const user = document.getElementById("userInput").value;
const message = document.getElementById("messageInput").value;
await connection.invoke("SendMessage", user, message);
}

connection.start().catch(err => console.error(err));


</script>
</body>
</html>
""", "text/html"));

app.Run();

[JsonSerializable(typeof(string))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }

public class ChatHub : Hub


{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}

The preceding example produces a native Windows executable of 10 MB and a Linux


executable of 10.9 MB.

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

types results in a runtime exception at startup in development and in the


published app. For more information, see SignalR: Using IAsyncEnumerable<T>
and ChannelReader<T> with ValueTypes in native AOT (dotnet/aspnetcore
#56179) .
Strongly typed hubs aren't supported with Native AOT ( PublishAot ). Using strongly
typed hubs with Native AOT will result in warnings during build and publish, and a
runtime exception. Using strongly typed hubs with trimming ( PublishedTrimmed ) is
supported.
Only Task , Task<T> , ValueTask , or ValueTask<T> are supported for async return
types.

Minimal APIs
This section describes new features for minimal APIs.

Added InternalServerError and


InternalServerError<TValue> to TypedResults

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#

var app = WebApplication.Create();

app.MapGet("/", () => TypedResults.InternalServerError("Something went


wrong!"));

app.Run();

Call ProducesProblem and ProducesValidationProblem on


route groups
The ProducesProblem and ProducesValidationProblem extension methods have been
updated to support their use on route groups. These methods indicate that all
endpoints in a route group can return ProblemDetails or ValidationProblemDetails
responses for the purposes of OpenAPI metadata.

C#
var app = WebApplication.Create();

var todos = app.MapGroup("/todos")


.ProducesProblem();

todos.MapGet("/", () => new Todo(1, "Create sample app", false));


todos.MapPost("/", (Todo todo) => Results.Ok(todo));

app.Run();

record Todo(int Id, string Title, boolean IsCompleted);

Problem and ValidationProblem result types support


construction with IEnumerable<KeyValuePair<string,
object?>> values

Prior to .NET 9, constructing Problem and ValidationProblem result types in minimal


APIs required that the errors and extensions properties be initialized with an
implementation of IDictionary<string, object?> . In this release, these construction
APIs support overloads that consume IEnumerable<KeyValuePair<string, object?>> .

C#

var app = WebApplication.Create();

app.MapGet("/", () =>
{
var extensions = new List<KeyValuePair<string, object?>> { new("test",
"value") };
return TypedResults.Problem("This is an error with extensions",
extensions:
extensions);
});

Thanks to GitHub user joegoldman2 for this contribution!

OpenAPI
This section describes new features for OpenAPI

Built-in support for OpenAPI document generation


The OpenAPI specification is a standard for describing HTTP APIs. The standard allows
developers to define the shape of APIs that can be plugged into client generators, server
generators, testing tools, documentation, and more. In .NET 9, ASP.NET Core provides
built-in support for generating OpenAPI documents representing controller-based or
minimal APIs via the Microsoft.AspNetCore.OpenApi package.

The following highlighted code calls:

AddOpenApi to register the required dependencies into the app's DI container.

MapOpenApi to register the required OpenAPI endpoints in the app's routes.

C#

var builder = WebApplication.CreateBuilder();

builder.Services.AddOpenApi();

var app = builder.Build();

app.MapOpenApi();

app.MapGet("/hello/{name}", (string name) => $"Hello {name}"!);

app.Run();

Install the Microsoft.AspNetCore.OpenApi package in the project using the following


command:

.NET CLI

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

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

dotnet add package Microsoft.Extensions.ApiDescription.Server --prerelease


To modify the location of the emitted OpenAPI documents, set the target path in the
OpenApiDocumentsDirectory property in the app's project file:

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 .

Microsoft.AspNetCore.OpenApi supports trimming and


Native AOT
The new built-in OpenAPI support in ASP.NET Core now also supports trimming and
Native AOT.
Get started
Create a new ASP.NET Core Web API (Native AOT) project.

Console

dotnet new webapiaot

Add the Microsoft.AspNetCore.OpenAPI package.

Console

dotnet add package Microsoft.AspNetCore.OpenApi --prerelease

For this preview, you also need to add the latest Microsoft.OpenAPI package to avoid
trimming warnings.

Console

dotnet add package Microsoft.OpenApi

Update Program.cs to enable generating OpenAPI documents.

diff

+ builder.Services.AddOpenApi();

var app = builder.Build();

+ app.MapOpenApi();

Publish the app.

Console

dotnet publish

The app publishes using Native AOT without warnings.

Authentication and authorization


This section describes new features for authentication and authorization.
OpenIdConnectHandler adds support for Pushed
Authorization Requests (PAR)
We'd like to thank Joe DeCock from Duende Software for adding Pushed
Authorization Requests (PAR) to ASP.NET Core's OpenIdConnectHandler. Joe described
the background and motivation for enabling PAR in his API proposal as follows:

Pushed Authorization Requests (PAR) is a relatively new OAuth standard that


improves the security of OAuth and OIDC flows by moving authorization parameters
from the front channel to the back channel. Thats is, moving authorization
parameters from redirect URLs in the browser to direct machine to machine http
calls on the back end.

This prevents a cyberattacker in the browser from:

Seeing authorization parameters, which could leak PII.


Tampering with those parameters. For example, the cyberattacker could
change the scope of access being requested.

Pushing the authorization parameters also keeps request URLs short. Authorize
parameters can get very long when using more complex OAuth and OIDC features
such as Rich Authorization Requests . URLs that are long cause issues in many
browsers and networking infrastructures.

The use of PAR is encouraged by the FAPI working group within the OpenID
Foundation. For example, the FAPI2.0 Security Profile requires the use of PAR. This
security profile is used by many of the groups working on open banking (primarily
in Europe), in health care, and in other industries with high security requirements.

PAR is supported by a number of identity providers, including

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.

// The default value is PushedAuthorizationBehavior.UseIfAvailable.

// 'OpenIdConnectOptions' does not contain a definition for


'PushedAuthorizationBehavior'
// and no accessible extension method 'PushedAuthorizationBehavior'
accepting a first argument
// of type 'OpenIdConnectOptions' could be found
oidcOptions.PushedAuthorizationBehavior =
PushedAuthorizationBehavior.Disable;
});

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


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

OIDC and OAuth Parameter Customization


The OAuth and OIDC authentication handlers now have an
AdditionalAuthorizationParameters option to make it easier to customize authorization

message parameters that are usually included as part of the redirect query string. In
.NET 8 and earlier, this requires a custom OnRedirectToIdentityProvider callback or
overridden BuildChallengeUrl method in a custom handler. Here's an example of .NET 8
code:

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

The preceding example can now be simplified to the following code:

C#

builder.Services.AddAuthentication().AddOpenIdConnect(options =>
{
options.AdditionalAuthorizationParameters.Add("prompt", "login");
options.AdditionalAuthorizationParameters.Add("audience",
"https://api.example.com");
});

Configure HTTP.sys extended authentication flags


You can now configure the
HTTP_AUTH_EX_FLAG_ENABLE_KERBEROS_CREDENTIAL_CACHING and
HTTP_AUTH_EX_FLAG_CAPTURE_CREDENTIAL HTTP.sys flags by using the new
EnableKerberosCredentialCaching and CaptureCredentials properties on the HTTP.sys

AuthenticationManager to optimize how Windows authentication is handled. For


example:

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.

New HybridCache library

) Important
HybridCache is currently still in preview but will be fully released after .NET 9.0 in a

future minor release of .NET Extensions.

The HybridCache API bridges some gaps in the existing IDistributedCache and
IMemoryCache APIs. It also adds new capabilities, such as:

"Stampede" protection to prevent parallel fetches of the same work.


Configurable serialization.

HybridCache is designed to be a drop-in replacement for existing IDistributedCache

and IMemoryCache usage, and it provides a simple API for adding new caching code. It
provides a unified API for both in-process and out-of-process caching.

To see how the HybridCache API is simplified, compare it to code that uses
IDistributedCache . Here's an example of what using IDistributedCache looks like:

C#

public class SomeService(IDistributedCache cache)


{
public async Task<SomeInformation> GetSomeInformationAsync
(string name, int id, CancellationToken token = default)
{
var key = $"someinfo:{name}:{id}"; // Unique key for this
combination.
var bytes = await cache.GetAsync(key, token); // Try to get from
cache.
SomeInformation info;
if (bytes is null)
{
// Cache miss; get the data from the real source.
info = await SomeExpensiveOperationAsync(name, id, token);

// Serialize and cache it.


bytes = SomeSerializer.Serialize(info);
await cache.SetAsync(key, bytes, token);
}
else
{
// Cache hit; deserialize it.
info = SomeSerializer.Deserialize<SomeInformation>(bytes);
}
return info;
}

// This is the work we're trying to cache.


private async Task<SomeInformation> SomeExpensiveOperationAsync(string
name, int id,
CancellationToken token = default)
{ /* ... */ }
}

That's a lot of work to get right each time, including things like serialization. And in the
cache miss scenario, you could end up with multiple concurrent threads, all getting a
cache miss, all fetching the underlying data, all serializing it, and all sending that data to
the cache.

To simplify and improve this code with HybridCache , we first need to add the new library
Microsoft.Extensions.Caching.Hybrid :

XML

<PackageReference Include="Microsoft.Extensions.Caching.Hybrid"
Version="9.0.0" />

Register the HybridCache service, like you would register an IDistributedCache


implementation:

C#

builder.Services.AddHybridCache(); // Not shown: optional configuration API.

Now most caching concerns can be offloaded to HybridCache :

C#

public class SomeService(HybridCache cache)


{
public async Task<SomeInformation> GetSomeInformationAsync
(string name, int id, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(
$"someinfo:{name}:{id}", // Unique key for this combination.
async cancel => await SomeExpensiveOperationAsync(name, id,
cancel),
token: token
);
}
}

We provide a concrete implementation of the HybridCache abstract class via


dependency injection, but it's intended that developers can provide custom
implementations of the API. The HybridCache implementation deals with everything
related to caching, including concurrent operation handling. The cancel token here
represents the combined cancellation of all concurrent callers—not just the cancellation
of the caller we can see (that is, token ).

High throughput scenarios can be further optimized by using the TState pattern, to
avoid some overhead from captured variables and per-instance callbacks:

C#

public class SomeService(HybridCache cache)


{
public async Task<SomeInformation> GetSomeInformationAsync(string name,
int id, CancellationToken token = default)
{
return await cache.GetOrCreateAsync(
$"someinfo:{name}:{id}", // unique key for this combination
(name, id), // all of the state we need for the final call, if
needed
static async (state, token) =>
await SomeExpensiveOperationAsync(state.name, state.id,
token),
token: token
);
}
}

HybridCache uses the configured IDistributedCache implementation, if any, for

secondary out-of-process caching, for example, using Redis. But even without an
IDistributedCache , the HybridCache service will still provide in-process caching and

"stampede" protection.

A note on object reuse


In typical existing code that uses IDistributedCache , every retrieval of an object from
the cache results in deserialization. This behavior means that each concurrent caller gets
a separate instance of the object, which cannot interact with other instances. The result
is thread safety, as there's no risk of concurrent modifications to the same object
instance.

Because a lot of HybridCache usage will be adapted from existing IDistributedCache


code, HybridCache preserves this behavior by default to avoid introducing concurrency
bugs. However, a given use case is inherently thread-safe:

If the types being cached are immutable.


If the code doesn't modify them.

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.

Other HybridCache features

Like IDistributedCache , HybridCache supports removal by key with a RemoveKeyAsync


method.

HybridCache also provides optional APIs for IDistributedCache implementations, to

avoid byte[] allocations. This feature is implemented by the preview versions of the
Microsoft.Extensions.Caching.StackExchangeRedis and
Microsoft.Extensions.Caching.SqlServer packages.

Serialization is configured as part of registering the service, with support for type-
specific and generalized serializers via the WithSerializer and .WithSerializerFactory
methods, chained from the AddHybridCache call. By default, the library handles string
and byte[] internally, and uses System.Text.Json for everything else, but you can use
protobuf, xml, or anything else.

HybridCache supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET

Standard 2.0.

For more information about HybridCache , see HybridCache library in ASP.NET Core

Developer exception page improvements


The ASP.NET Core developer exception page is displayed when an app throws an
unhandled exception during development. The developer exception page provides
detailed information about the exception and request.

Preview 3 added endpoint metadata to the developer exception page. ASP.NET Core
uses endpoint metadata to control endpoint behavior, such as routing, response
caching, rate limiting, OpenAPI generation, and more. The following image shows the
new metadata information in the Routing section of the developer exception page:
While testing the developer exception page, small quality of life improvements were
identified. They shipped in Preview 4:

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

Fix for 503's during app recycle in IIS


By default there is now a 1 second delay between when IIS is notified of a recycle or
shutdown and when ANCM tells the managed server to start shutting down. The delay is
configurable via the ANCM_shutdownDelay environment variable or by setting the
shutdownDelay handler setting. Both values are in milliseconds. The delay is mainly to
reduce the likelihood of a race where:

IIS hasn't started queuing requests to go to the new app.


ANCM starts rejecting new requests that come into the old app.

Slower machines or machines with heavier CPU usage may want to adjust this value to
reduce 503 likelihood.

Example of setting shutdownDelay :

XML

<aspNetCore processPath="dotnet" arguments="myapp.dll"


stdoutLogEnabled="false" stdoutLogFile=".logsstdout">
<handlerSettings>
<!-- Milliseconds to delay shutdown by.
this doesn't mean incoming requests will be delayed by this amount,
but the old app instance will start shutting down after this timeout
occurs -->
<handlerSetting name="shutdownDelay" value="5000" />
</handlerSettings>
</aspNetCore>

The fix is in the globally installed ANCM module that comes from the hosting bundle.

ASP0026: Analyzer to warn when [Authorize] is


overridden by [AllowAnonymous] from "farther away"
It seems intuitive that an [Authorize] attribute placed "closer" to an MVC action than an
[AllowAnonymous] attribute would override the [AllowAnonymous] attribute and force
authorization. However, this is not necessarily the case. What does matter is the relative
order of the attributes.

The following code shows examples where a closer [Authorize] attribute gets
overridden by an [AllowAnonymous] attribute that is farther away.

C#

[AllowAnonymous]
public class MyController
{
[Authorize] // Overridden by the [AllowAnonymous] attribute on the class
public IActionResult Private() => null;
}

C#

[AllowAnonymous]
public class MyControllerAnon : ControllerBase
{
}

[Authorize] // Overridden by the [AllowAnonymous] attribute on


MyControllerAnon
public class MyControllerInherited : MyControllerAnon
{
}

public class MyControllerInherited2 : MyControllerAnon


{
[Authorize] // Overridden by the [AllowAnonymous] attribute on
MyControllerAnon
public IActionResult Private() => null;
}
C#

[AllowAnonymous]
[Authorize] // Overridden by the preceding [AllowAnonymous]
public class MyControllerMultiple : ControllerBase
{
}

In .NET 9 Preview 6, we've introduced an analyzer that will highlight instances like these
where a closer [Authorize] attribute gets overridden by an [AllowAnonymous] attribute
that is farther away from an MVC action. The warning points to the overridden
[Authorize] attribute with the following message:

ASP0026 [Authorize] overridden by [AllowAnonymous] from farther away

The correct action to take if you see this warning depends on the intention behind the
attributes. The farther away [AllowAnonymous] attribute should be removed if it's
unintentionally exposing the endpoint to anonymous users. If the [AllowAnonymous]
attribute was intended to override a closer [Authorize] attribute, you can repeat the
[AllowAnonymous] attribute after the [Authorize] attribute to clarify the intent.

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;
}

Improved Kestrel connection metrics


We've made a significant improvement to Kestrel's connection metrics by including
metadata about why a connection failed. The kestrel.connection.duration metric now
includes the connection close reason in the error.type attribute.
Here is a small sample of the error.type values:

tls_handshake_failed - The connection requires TLS, and the TLS handshake

failed.
connection_reset - The connection was unexpectedly closed by the client while

requests were in progress.


request_headers_timeout - Kestrel closed the connection because it didn't receive

request headers in time.


max_request_body_size_exceeded - Kestrel closed the connection because uploaded

data exceeded max size.

Previously, diagnosing Kestrel connection issues required a server to record detailed,


low-level logging. However, logs can be expensive to generate and store, and it can be
difficult to find the right information among the noise.

Metrics are a much cheaper alternative that can be left on in a production environment
with minimal impact. Collected metrics can drive dashboards and alerts. Once a problem
is identified at a high-level with metrics, further investigation using logging and other
tooling can begin.

We expect improved connection metrics to be useful in many scenarios:

Investigating performance issues caused by short connection lifetimes.


Observing ongoing external attacks on Kestrel that impact performance and
stability.
Recording attempted external attacks on Kestrel that Kestrel's built-in security
hardening prevented.

For more information, see ASP.NET Core metrics.

Customize Kestrel named pipe endpoints


Kestrel's named pipe support has been improved with advanced customization options.
The new CreateNamedPipeServerStream method on the named pipe options allows pipes
to be customized per-endpoint.

An example of where this is useful is a Kestrel app that requires two pipe endpoints with
different access security. The CreateNamedPipeServerStream option can be used to create
pipes with custom security settings, depending on the pipe name.

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

ExceptionHandlerMiddleware option to choose the status


code based on the exception type
A new option when configuring the ExceptionHandlerMiddleware enables app
developers to choose what status code to return when an exception occurs during
request handling. The new option changes the status code being set in the
ProblemDetails response from the ExceptionHandlerMiddleware .

C#

app.UseExceptionHandler(new ExceptionHandlerOptions
{
StatusCodeSelector = ex => ex is TimeoutException
? StatusCodes.Status503ServiceUnavailable
: StatusCodes.Status500InternalServerError,
});

Opt-out of HTTP metrics on certain endpoints and


requests
.NET 9 introduces the ability to opt-out of HTTP metrics for specific endpoints and
requests. Opting out of recording metrics is beneficial for endpoints frequently called by
automated systems, such as health checks. Recording metrics for these requests is
generally unnecessary.

HTTP requests to an endpoint can be excluded from metrics by adding metadata. Either:

Add the [DisableHttpMetrics] attribute to the Web API controller, SignalR hub or
gRPC service.
Call DisableHttpMetrics when mapping endpoints in app startup:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddHealthChecks();

var app = builder.Build();


app.MapHealthChecks("/healthz").DisableHttpMetrics();
app.Run();

The MetricsDisabled property has been added to IHttpMetricsTagsFeature for:

Advanced scenarios where a request doesn't map to an endpoint.


Dynamically disabling metrics collection for specific HTTP requests.

C#

// Middleware that conditionally opts-out HTTP requests.


app.Use(async (context, next) =>
{
var metricsFeature = context.Features.Get<IHttpMetricsTagsFeature>();
if (metricsFeature != null &&
context.Request.Headers.ContainsKey("x-disable-metrics"))
{
metricsFeature.MetricsDisabled = true;
}

await next(context);
});

Data Protection support for deleting keys


Prior to .NET 9, data protection keys were not deletable by design, to prevent data loss.
Deleting a key renders its protected data irretrievable. Given their small size, the
accumulation of these keys generally posed minimal impact. However, to accommodate
extremely long-running services, we have introduced the option to delete keys.
Generally, only old keys should be deleted. Only delete keys when you can accept the
risk of data loss in exchange for storage savings. We recommend data protection keys
should not be deleted.

C#

using Microsoft.AspNetCore.DataProtection.KeyManagement;

var services = new ServiceCollection();


services.AddDataProtection();

var serviceProvider = services.BuildServiceProvider();

var keyManager = serviceProvider.GetService<IKeyManager>();

if (keyManager is IDeletableKeyManager deletableKeyManager)


{
var utcNow = DateTimeOffset.UtcNow;
var yearAgo = utcNow.AddYears(-1);

if (!deletableKeyManager.DeleteKeys(key => key.ExpirationDate <


yearAgo))
{
Console.WriteLine("Failed to delete keys.");
}
else
{
Console.WriteLine("Old keys deleted successfully.");
}
}
else
{
Console.WriteLine("Key manager does not support deletion.");
}

Middleware supports Keyed DI


Middleware now supports Keyed DI in both the constructor and the
Invoke / InvokeAsync method:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddKeyedSingleton<MySingletonClass>("test");
builder.Services.AddKeyedScoped<MyScopedClass>("test2");

var app = builder.Build();


app.UseMiddleware<MyMiddleware>();
app.Run();
internal class MyMiddleware
{
private readonly RequestDelegate _next;

public MyMiddleware(RequestDelegate next,


[FromKeyedServices("test")] MySingletonClass service)
{
_next = next;
}

public Task Invoke(HttpContext context,


[FromKeyedServices("test2")]
MyScopedClass scopedService) => _next(context);
}

Trust the ASP.NET Core HTTPS development certificate on Linux


On Ubuntu and Fedora based Linux distros, dotnet dev-certs https --trust now
configures ASP.NET Core HTTPS development certificate as a trusted certificate for:

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 OpenSSL, the dev-certs tool:

Puts the certificate in ~/.aspnet/dev-certs/trust


Runs a simplified version of OpenSSL's c_rehash tool on the directory.
Asks the user to update the SSL_CERT_DIR environment variable.

To establish trust in dotnet, the tool puts the certificate in the My/Root certificate store.

To establish trust in NSS databases , if any, the tool searches the home directory for
Firefox profiles, ~/.pki/nssdb , and ~/snap/chromium/current/.pki/nssdb . For each
directory found, the tool adds an entry to the nssdb .

Templates updated to latest Bootstrap, jQuery, and


jQuery Validation versions
The ASP.NET Core project templates and libraries have been updated to use the latest
versions of Bootstrap, jQuery, and jQuery Validation, specifically:
Bootstrap 5.3.3
jQuery 3.7.1
jQuery Validation 1.21.0
What's new in .NET MAUI for .NET 9
Article • 11/08/2024

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:

.NET MAUI 9 RC2


.NET MAUI 9 RC1
.NET MAUI 9 Preview 7
.NET MAUI 9 Preview 6
.NET MAUI 9 Preview 5
.NET MAUI 9 Preview 4
.NET MAUI 9 Preview 3
.NET MAUI 9 Preview 2
.NET MAUI 9 Preview 1

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

Minimum deployment targets


.NET MAUI 9 requires minimum deployment targets of iOS 12.2, and Mac Catalyst 15.0
(macOS 12.0). Android and Windows minimum deployment targets remain the same.
For more information, see Supported platforms for .NET MAUI apps.
New controls
.NET MAUI 9 includes two new controls.

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.

To build a .NET MAUI app with HybridWebView you need:

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.

For more information, see HybridWebView.

Titlebar for Windows


The TitleBar control provides the ability to add a custom title bar to your app on
Windows:

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>

An example of its use in C# is:

C#

Window window = new Window


{
TitleBar = new TitleBar
{
Icon = "titlebar_icon.png"
Title = "My App",
Subtitle = "Demo"
Content = new SearchBar { ... }
}
};

A TitleBar is highly customizable through its Content, LeadingContent, and


TrailingContent properties:

XAML

<TitleBar Title="My App"


BackgroundColor="#512BD4"
HeightRequest="48">
<TitleBar.Content>
<SearchBar Placeholder="Search"
MaximumWidthRequest="300"
HorizontalOptions="Fill"
VerticalOptions="Center" />
</TitleBar.Content>
<TitleBar.TrailingContent>
<ImageButton HeightRequest="36"
WidthRequest="36"
BorderWidth="0"
Background="Transparent">
<ImageButton.Source>
<FontImageSource Size="16"
Glyph="&#xE713;"
FontFamily="SegoeMDL2"/>
</ImageButton.Source>
</ImageButton>
</TitleBar.TrailingContent>
</TitleBar>

The following screenshot shows the resulting appearance:

7 Note

Mac Catalyst support for the TitleBar control will be added in a future release.

For more information, see TitleBar.

Control enhancements
.NET MAUI 9 includes control enhancements.

BackButtonBehavior OneWay binding mode


The binding mode for IsVisible and IsEnabled on a BackButtonBehavior in a Shell app
is now BindingMode.OneWay instead of BindingMode.OneTime . This enables you to more
easily control the behavior of the back button at runtime, with data bindings:

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);

If you encounter hangs on Android with BlazorWebView you should enable an


AppContext switch in the CreateMauiApp method in your MauiProgram class:

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.

CollectionView and CarouselView


.NET MAUI 9 includes two optional new handlers on iOS and Mac Catalyst that bring
performance and stability improvements to CollectionView and CarouselView . These
handlers are based on UICollectionView APIs.

To opt into using these handlers, add the following code to your MauiProgram class:
C#

#if IOS || MACCATALYST


builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<Microsoft.Maui.Controls.CollectionView,
Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2>();
handlers.AddHandler<Microsoft.Maui.Controls.CarouselView,
Microsoft.Maui.Controls.Handlers.Items2.CarouselViewHandler2>();
});
#endif

ContentPage
In .NET MAUI 9, the HideSoftInputOnTapped property is also supported on Mac Catalyst,
as well and Android and iOS.

Soft keyboard input support


.NET MAUI 9 adds new soft keyboard input support for Password , Date , and Time . These
can be enabled on Editor and Entry controls:

XAML

<Entry Keyboard="Date" />

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

<Label Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In


facilisis nulla eu felis fringilla vulputate."
HorizontalTextAlignment="Justify"/>

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.

Compiled bindings in code


Bindings written in code typically use string paths that are resolved at runtime with
reflection, and the overhead of doing this varies from platform to platform. .NET MAUI 9
introduces an additional SetBinding extension method that defines bindings using a
Func argument instead of a string path:

C#

// in .NET 8
MyLabel.SetBinding(Label.TextProperty, "Text");

// in .NET 9
MyLabel.SetBinding(Label.TextProperty, static (Entry entry) => entry.Text);

This compiled binding approach provides the following benefits:

Improved data binding performance by resolving binding expressions at compile-


time rather than runtime.
A better developer troubleshooting experience because invalid bindings are
reported as build errors.
Intellisense while editing.

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#

// Valid: Property access


static (PersonViewModel vm) => vm.Name;
static (PersonViewModel vm) => vm.Address?.Street;

// Valid: Array and indexer access


static (PersonViewModel vm) => vm.PhoneNumbers[0];
static (PersonViewModel vm) => vm.Config["Font"];
// Valid: Casts
static (Label label) => (label.BindingContext as PersonViewModel).Name;
static (Label label) => ((PersonViewModel)label.BindingContext).Name;

// Invalid: Method calls


static (PersonViewModel vm) => vm.GetAddress();
static (PersonViewModel vm) => vm.Address?.ToString();

// Invalid: Complex expressions


static (PersonViewModel vm) => vm.Address?.Street + " " + vm.Address?.City;
static (PersonViewModel vm) => $"Name: {vm.Name}";

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.

Compiled bindings in XAML


In .NET MAUI 8, compiled bindings are disabled for any XAML binding expressions that
define the Source property, and are unsupported on multi-bindings. These restrictions
have been removed in .NET MAUI 9. For information about compiling XAML binding
expressions that define the Source property, see Compile bindings that define the
Source property.

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:

Automatic , which indicates that handlers will be disconnected automatically. This is

the default value of the HandlerProperties.DisconnectPolicy attached property.


Manual , which indicates that handlers will have to be disconnected manually by

invoking the DisconnectHandler() implementation.

The following example shows setting the HandlerProperties.DisconnectPolicy attached


property:

XAML

<controls:Video x:Name="video"
HandlerProperties.DisconnectPolicy="Manual"
Source="video.mp4"
AutoPlay="False" />

The equivalent C# code is:

C#

Video video = new Video


{
Source = "video.mp4",
AutoPlay = false
};
HandlerProperties.SetDisconnectPolicy(video,
HandlerDisconnectPolicy.Manual);

In addition, there's a DisconnectHandlers extension method that disconnects handlers


from a given IView:

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:

Reduced app package size, typically up to 2.5x smaller.


Faster startup time, typically up to 2x faster.
Faster build time.

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#

var mauiApp = MauiProgram.CreateMauiApp();

#if ANDROID
var mauiContext = new MauiContext(mauiApp.Services, window);
#else
var mauiContext = new MauiContext(mauiApp.Services);
#endif

var mauiView = new MyMauiContent();


var nativeView = mauiView.ToPlatform(mauiContext);

Alternatively, you can use the ToPlatformEmbedded method, passing in the Window for the
platform on which the app is running:

C#

var mauiApp = MauiProgram.CreateMauiApp();


var mauiView = new MyMauiContent();
var nativeView = mauiView.ToPlatformEmbedded(mauiApp, window);

In both examples, nativeView is a platform-specific version of mauiView .

To bootstrap a native embedded app in .NET MAUI 9, call the UseMauiEmbeddedApp


extension method on your MauiAppBuilder object:

C#
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();

builder
.UseMauiEmbeddedApp<App>();

return builder.Build();
}
}

For more information, see Native embedding.

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.

The template can also be used from dotnew new :

.NET CLI

dotnet new maui-blazor-web -n AllTheTargets

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.

Trimming feature switches


.NET MAUI has trimmer directives, known as feature switches, that make it possible to
preserve the code for features that aren't trim safe. These trimmer directives can be
used when the $(TrimMode) build property is set to full , as well as for NativeAOT:

ノ Expand table

MSBuild property Description

MauiEnableVisualAssemblyScanning When set to true , .NET MAUI will


scan assemblies for types
implementing IVisual and for
[assembly:Visual(...)] attributes,
and will register these types. By
default, this build property is set to
false .

MauiShellSearchResultsRendererDisplayMemberNameSupported When set to false , the value of


SearchHandler.DisplayMemberName
will be ignored. Instead, you should
provide an ItemTemplate to define
the appearance of SearchHandler
MSBuild property Description

results. By default, this build


property is set to true .

MauiQueryPropertyAttributeSupport When set to false ,


[QueryProperty(...)] attributes
won't be used to set property
values when navigating. Instead,
you should implement the
IQueryAttributable interface to
accept query parameters. By
default, this build property is set to
true .

MauiImplicitCastOperatorsUsageViaReflectionSupport When set to false , .NET MAUI


won't look for implicit conversion
operators when converting values
from one type to another. This can
affect bindings between properties
with different types, and setting a
property value of a bindable object
with a value of a different type.
Instead, you should define a
TypeConverter for your type and
attach it to the type using the
TypeConverterAttribute attribute.
By default, this build property is set
to true .

_MauiBindingInterceptorsSupport When set to false , .NET MAUI


won't intercept any calls to the
SetBinding methods and won't try
to compile them. By default, this
build property is set to true .

MauiEnableXamlCBindingWithSourceCompilation When set to true , .NET MAUI will


compile all bindings, including
those where the Source property is
used. If you enable this feature
ensure that all bindings have the
correct x:DataType so that they
compile, or clear the data type with
x:Data={x:Null}} if the binding
shouldn't be compiled. By default,
this build property is only set to
true when full trimming or Native
AOT deployment is enabled.
These MSBuild properties also have equivalent AppContext switches:

The MauiEnableVisualAssemblyScanning MSBuild property has an equivalent


AppContext switch named
Microsoft.Maui.RuntimeFeature.IsIVisualAssemblyScanningEnabled .

The MauiShellSearchResultsRendererDisplayMemberNameSupported MSBuild property


has an equivalent AppContext switch named
Microsoft.Maui.RuntimeFeature.IsShellSearchResultsRendererDisplayMemberNameSup

ported .

The MauiQueryPropertyAttributeSupport MSBuild property has an equivalent


AppContext switch named
Microsoft.Maui.RuntimeFeature.IsQueryPropertyAttributeSupported .

The MauiImplicitCastOperatorsUsageViaReflectionSupport MSBuild property has


an equivalent AppContext switch named
Microsoft.Maui.RuntimeFeature.IsImplicitCastOperatorsUsageViaReflectionSupport

ed .

The _MauiBindingInterceptorsSupport MSBuild property has an equivalent


AppContext switch named
Microsoft.Maui.RuntimeFeature.AreBindingInterceptorsSupported .

The MauiEnableXamlCBindingWithSourceCompilation MSBuild property has an


equivalent AppContext switch named
Microsoft.Maui.RuntimeFeature.MauiEnableXamlCBindingWithSourceCompilationEnabl
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.

Windows app deployment


When debugging and deploying a new .NET MAUI project to Windows, the default
behavior in .NET MAUI 9 is to deploy an unpackaged app. For more information, see
Deploy and debug your .NET MAUI app on Windows.

XAML compiler error codes


In .NET MAUI 9, the XAML compiler error codes have changed their prefix from XFC to
XC . Ensure that you update the $(WarningsAsErrors) , $(WarningsNotAsErrors) , and
$(NoWarn) build properties in your app's project files, if used, to reference the new

prefix.

XAML markup extensions


All classes that implement IMarkupExtension, IMarkupExtension<T>, IValueProvider, and
IExtendedTypeConverter need to be annotated with either the RequireServiceAttribute
or AcceptEmptyServiceProviderAttribute. This is required due to a XAML compiler
optimization introduced in .NET MAUI 9 that enables the generation of more efficient
code, which helps reduce the app size and improve runtime performance.

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

dotnet build /t:xcsync-generate


/p:xcSyncProjectFile=<PROJECT>
/p:xcSyncXcodeFolder=<TARGET_XCODE_DIRECTORY>
/p:xcSyncTargetFrameworkMoniker=<FRAMEWORK>
/p:xcSyncVerbosity=<LEVEL>

For more information, see Xcode sync.

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#

public partial class App : Application


{
public App()
{
InitializeComponent();
}

protected override Window CreateWindow(IActivationState?


activationState)
{
return new Window(new AppShell());
}
}

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

with multiple windows, use the Application.Current.Windows collection to identify the


correct window and then access the Page property. In addition, each element features a
Window property, that's accessible when the element is part of the current window, from

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.

Legacy measure calls


The following VisualElement measure methods 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.

As a replacement, the VisualElement.Measure(Double, Double) method has been


introduced. This method returns the minimum size that an element needs in order to be
displayed on a device. Margins are excluded from the measurement, but are returned
with the size. This is the preferred method to call when measuring a view.

In addition, the SizeRequest struct is obsoleted. Instead, Size should be used.

Upgrade from .NET 8 to .NET 9


To upgrade your .NET MAUI projects from .NET 8 to .NET 9, first install .NET 9 and the
.NET MAUI workload with Visual Studio 17.12+ , or with Visual Studio Code and the
.NET MAUI extension and .NET and the .NET MAUI workloads, or with the standalone
installer and the dotnet workload install maui command.

Update project file


To update your .NET MAUI app from .NET 8 to .NET 9 open the app's project file (.csproj)
and change the Target Framework Monikers (TFMs) from 8 to 9. If you're using a TFM
such as net8.0-ios15.2 be sure to match the platform version or remove it entirely. The
following example shows the TFMs for a .NET 8 project:

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>

The following example shows the TFMs for a .NET 9 project:


XML

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

If your app's project file references a .NET 8 version of the Microsoft.Maui.Controls


NuGet package, either directly or through the $(MauiVersion) build property, update
this to a .NET 9 version. Then, remove the package reference for the
Microsoft.Maui.Controls.Compatibility NuGet package, provided that your app doesn't
use any types from this package. In addition, update the package reference for the
Microsoft.Extensions.Logging.Debug NuGet package to the latest .NET 9 release.

If your app targets iOS or Mac Catalyst, update the $(SupportedOSPlatformVersion)


build properties for these platforms to 15.0:

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.

Update XAML compiler error codes


XAML compiler error codes have changed their prefix from XFC to XC , so update the
$(WarningsAsErrors) , $(WarningsNotAsErrors) , and $(NoWarn) build properties in your

app's project file, if used, to reference the new prefix.

Address new XAML compiler warnings for compiled


bindings
Build warnings will be produced for bindings that don't use compiled bindings, and
these will need to be addressed. For more information, see XAML compiled bindings
warnings.

Update XAML markup extensions


XAML markup extensions will need to be annotated with either the
RequireServiceAttribute or AcceptEmptyServiceProviderAttribute. This is required due to
a XAML compiler optimization that enables the generation of more efficient code, which
helps reduce the app size and improve runtime performance. For more information, see
Service providers.

Address deprecated APIs


.NET MAUI 9 deprecates some APIs, which will be completely removed in a future
release. Therefore, address any build warnings about deprecated APIs. For more
information, see Deprecated APIs.

Adopt compiled bindings that set the Source property


You can opt into compiling bindings that set the Source property, to take advantage of
better runtime performance. For more information, see Compile bindings that define the
Source property.

Adopt compiled bindings in C#


You can opt into compiling binding expressions that are declared in code, to take
advantage of better runtime performance. For more information, see Compiled bindings
in code.

Adopt full trimming


You can adopt into using full trimming, to reduce the overall size of your app, by setting
the $(TrimMode) MSBuild property to full . For more information, see Trim a .NET MAUI
app.

Adopt NativeAOT deployment on supported platforms


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. For more information, see Native AOT deployment on iOS and Mac
Catalyst.

.NET for Android


.NET for Android in .NET 9, which adds support for API 35, includes work to reduce build
times, and to improve the trimability of apps to reduce size and improve performance.
For more information about .NET for Android in .NET 9, see the following release notes:

.NET for Android 9 RC2


.NET for Android 9 RC1
.NET for Android 9 Preview 7
.NET for Android 9 Preview 6
.NET for Android 9 Preview 5
.NET for Android 9 Preview 4
.NET for Android 9 Preview 3
.NET for Android 9 Preview 2
.NET for Android 9 Preview 1

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

The additional metadata will be ignored by other platforms.

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

<MauiAsset Update="Resources\Raw\MyLargeAsset.txt" AssetPack="myassetpack"


/>

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

<MauiAsset Update="Resources\Raw\myvideo.mp4" AssetPack="myassetpack"


DeliveryType="FastFollow" />

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.

64-bit architectures by default


.NET for Android in .NET 9 no longer builds the following runtime identifiers (RIDs) by
default:

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


Improvements to Android marshal methods in .NET 9 has made the feature work more
reliably in applications but is not yet the default. Enabling this feature has resulted in a
~10% improvement in performance in a test app .

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.

For more information, see Trimming granularity.

.NET for iOS


.NET 9 on iOS, tvOS, Mac Catalyst, and macOS uses Xcode 16.0 for the following
platform versions:

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:

.NET 9.0.1xx RC2


.NET 9.0.1xx RC1
.NET 9.0.1xx Preview 7
.NET 9.0.1xx Preview 6
.NET 9.0.1xx Preview 5
.NET 9.0.1xx Preview 4
.NET 9.0.1xx Preview 3
.NET 9.0.1xx Preview 2
.NET 9.0.1xx Preview 1

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

An app project should always target the latest iOS SDK.

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.

For more information, see Trimming granularity.

Native AOT for iOS & Mac Catalyst


In .NET for iOS 9, native Ahead of Time (AOT) compilation for iOS and Mac Catalyst
takes advantage of full trimming to reduce your app's package size and startup
performance. NativeAOT builds upon full trimming, by also opting into a new runtime.

) Important

Your app and it's dependencies must be fully trimmable in order to utilize this
feature.

NativeAOT requires applications to be built with zero trimmer warnings, in order to


prove the application will work correctly at runtime.

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.

Azure Cosmos DB for NoSQL


EF 9.0 brings substantial improvements to the EF Core provider for Azure Cosmos DB;
significant parts of the provider have been rewritten to provide new functionality, allow
new forms of queries, and better align the provider with Azure Cosmos DB best
practices. The main high-level improvements are listed below; for a full list, see this epic
issue .

2 Warning

As part of the improvements going into the provider, a number of high-impact


breaking changes had to be made; if you are upgrading an existing application,
please read the breaking changes section carefully.
Improvements querying with partition keys and
document IDs
Each document stored in an Azure Cosmos DB database has a unique resource ID. In
addition, each document can contain a "partition key" which determines the logical
partitioning of data such that the database can be effectively scaled. More information
on choosing partition keys can be found in Partitioning and horizontal scaling in Azure
Cosmos DB.

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#

var sessions = await context.Sessions


.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();

In this query, the provider automatically recognizes the comparison on PartitionKey ; if


we examine the logs, we'll see the following:

Console

Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-


338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")

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();

The logs show the following for this query:

Console

Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-


67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'

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.

Hierarchical partition keys

 Tip

The code shown here comes from HierarchicalPartitionKeysSample.cs .

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#

public class UserSession


{
// Item ID
public Guid Id { get; set; }

// 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#

var tenantId = "Microsoft";


var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions


.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();

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

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100]


(Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition
'["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND
CONTAINS(c["Username"], "a"))

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.

Significantly improved LINQ querying capabilities


In EF 9.0, the LINQ translation capabilities of the the Azure Cosmos DB provider have
been greatly expanded, and the provider can now execute significantly more query
types. The full list of query improvements is too long to list, but here are the main
highlights:

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.

Support for aggregate operators such as Count and Sum .


Additional function translations (see the function mappings documentation for the
full list of supported translations):
Translations for DateTime and DateTimeOffset component members
( DateTime.Year , DateTimeOffset.Month ...).
EF.Functions.IsDefined and EF.Functions.CoalesceUndefined now allow

dealing with undefined values.


string.Contains , StartsWith and EndsWith now support
StringComparison.OrdinalIgnoreCase .

For the full list of querying improvements, see this issue :

Improved modeling aligned to Azure Cosmos DB and


JSON standards
EF 9.0 maps to Azure Cosmos DB documents in more natural ways for a JSON-based
document database, and helps interoperate with other systems accessing your
documents. Although this entails breaking changes, APIs exist which allow reverting
back to the pre-9.0 behavior in all cases.

Simplified id properties without discriminators

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.

Discriminator property renamed to $type


The default discriminator property was previously named Discriminator . EF 9.0 changes
the default to $type :

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.

Vector similarity search (preview)


Azure Cosmos DB now offers preview support for vector similarity search. Vector search
is a fundamental part of some application types, including AI, semantic search and
others. Azure Cosmos DB allows you to store vectors directly in your documents
alongside the rest of your data, meaning you can perform all of your queries against a
single database. This can considerably simplify your architecture and remove the need
for an additional, dedicated vector database solution in your stack. To learn more about
Azure Cosmos DB vector search, see the documentation.

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#

public class Blog


{
...
public float[] Vector { get; set; }
}

public class BloggingContext


{
...

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}

Once that's done, use the EF.Functions.VectorDistance() function in LINQ queries to


perform vector similarity search:

c#

var blogs = await context.Blogs


.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();

For more information, see the documentation on vector search.

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#

var firstPage = await context.Posts


.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);

var continuationToken = firstPage.ContinuationToken;


foreach (var post in page.Values)
{
// Display/send the posts to the user
}

The new ToPageAsync operator returns a CosmosPage , which exposes a continuation


token that can be used to efficiently resume the query at a later point, fetching the next
10 items:

c#

var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10,


continuationToken);

For more information, see the documentation section on pagination.

FromSql for safer SQL querying


The Azure Cosmos DB provider has allowed SQL querying via FromSqlRaw. However,
that API can be susceptible to SQL injection attacks when user-provided data is
interpolated or concatenated into the SQL. In EF 9.0, you can now use the new FromSql
method, which always integrates parameterized data as a parameter outside the SQL:

c#

var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();

For more information, see the documentation section on pagination.

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 is now blocked by default


Azure Cosmos DB for NoSQL does not support synchronous (blocking) APIs from
application code. Previously, EF masked this by blocking for you on async calls. However,
this both encourages synchronous I/O use, which is bad practice, and may cause
deadlocks . Therefore, starting with EF 9, an exception is thrown when synchronous
access is attempted. For example:

Synchronous I/O can still be used for now by configuring the warning level
appropriately. For example, in OnConfiguring on your DbContext type:
C#

protected override void OnConfiguring(DbContextOptionsBuilder


optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b =>
b.Ignore(CosmosEventId.SyncNotSupported));

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!

AOT and pre-compiled queries

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.

For example, given a program with the following EF query:

c#

var blogs = await context.Blogs.Where(b => b.Name == "foo").ToListAsync();

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#

var relationalCommandTemplate = ((IRelationalCommandTemplate)(new


RelationalCommand(materializerLiftableConstantContext.CommandBuilderDependen
cies, "SELECT [b].[Id], [b].[Name]\nFROM [Blogs] AS [b]\nWHERE [b].[Name] =
N'foo'", new IRelationalParameter[] { })));

In addition, the same interceptor contains code to materialize your .NET object from
database results:

c#

var instance = new Blog();


UnsafeAccessor_Blog_Id_Set(instance) = dataReader.GetInt32(0);
UnsafeAccessor_Blog_Name_Set(instance) = dataReader.GetString(1);

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.

See the NativeAOT documentation page for more details.

LINQ and SQL translation


Like with every release, EF9 includes a large number of improvements to the LINQ
querying capabilities. New queries can be translated, and many SQL translations for
supported scenarios have been improved, for both better performance and readability.

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!

Complex types: GroupBy and ExecuteUpdate support

GroupBy
 Tip

The code shown here comes from ComplexTypesSample.cs .

EF9 supports grouping by a complex type instance. For example:

C#

var groupedAddresses = await context.Stores


.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();

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

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].


[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode],
COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].
[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

ExecuteUpdate

 Tip

The code shown here comes from ExecuteUpdateSample.cs .

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#

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley",


"Norfolk", "NR20 4DR");

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.

Prune unneeded elements from SQL


Previously, EF sometimes produced SQL which contained elements that weren't actually
needed; in most cases, these were possibly needed at an earlier stage of SQL processing,
and were left behind. EF9 now prunes most such elements, resulting in more compact
and, in some cases, more efficient SQL.

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#

public class Order


{
public int Id { get; set; }
...

public Customer Customer { get; set; }


}

public class DiscountedOrder : Order


{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...

public List<Order> Orders { get; set; }


}

public class BlogContext : DbContext


{
...

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}

If we then execute the following query to get all Customers with at least one Order:

C#

var customers = await context.Customers.Where(o =>


o.Orders.Any()).ToListAsync();

EF8 generated the following SQL:

SQL

SELECT [c].[Id], [c].[Name]


FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])

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#

SELECT [c].[Id], [c].[Name]


FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Projection pruning
Similarly, let's examine the following query:

C#

var orders = await context.Orders


.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();

On EF8, this query generated the following SQL:

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.

Translations involving GREATEST/LEAST

 Tip

The code shown here comes from LeastGreatestSample.cs .


Several new translations have been introduced that use the GREATEST and LEAST SQL
functions.

) 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#

var walksUsingMin = await context.Walks


.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) >
4)
.ToListAsync();

This query is translated to the following SQL when using EF9 executing against SQL
Server 2022:

SQL

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].


[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min and Math.Max can also be used on the values of a primitive collection. For

example:

C#

var pubsInlineMax = await context.Pubs


.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
This query is translated to the following SQL when using EF9 executing against SQL
Server 2022:

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

Finally, RelationalDbFunctionsExtensions.Least and


RelationalDbFunctionsExtensions.Greatest can be used to directly invoke the Least or

Greatest function in SQL. For example:

C#

var leastCount = await context.Pubs


.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count,
e.Beers.Length))
.ToListAsync();

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]

Force or prevent query parameterization

 Tip

The code shown here comes from QuerySample.cs .

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#

async Task<List<Post>> GetPosts(int id)


=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();

This translates to the following SQL and parameters when using Azure SQL:

Output

Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text',


CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].
[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

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#

async Task<List<Post>> GetPostsForceConstant(int id)


=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();

The translation now contains a constant for the id value:

Output

Executed DbCommand (1ms) [Parameters=[], CommandType='Text',


CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].
[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

The EF.Parameter method


EF9 introduces the EF.Parameter method to do the opposite. That is, force EF to use a
parameter even if the value is a constant in code. For example:

C#

async Task<List<Post>> GetPostsForceParameter(int id)


=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();

The translation now contains a parameter for the ".NET Blog" string:

Output

Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000),


@__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].
[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

Parameterized primitive collections

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#

async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)


=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();

This will result in the following translation on SQL Server:


Output

Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)],


CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].
[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].
[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)

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.

The following query uses EF.Constant to that effect:

C#

async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)


=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();

The resulting SQL is as follows:

SQL

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].


[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].
[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)

Moreover, EF9 introduces TranslateParameterizedCollectionsToConstants context


option that can be used to prevent primitive collection parameterization for all queries.
We also added a complementing TranslateParameterizedCollectionsToParameters
which forces parameterization of primitive collections explicitly (this is the default
behavior).

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

Inlined uncorrelated subqueries

 Tip

The code shown here comes from QuerySample.cs .

In EF8, an IQueryable referenced in another query may be executed as a separate


database roundtrip. For example, consider the following LINQ query:

C#

var dotnetPosts = context


.Posts
.Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts


.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArray();

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%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].


[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
In EF9, the IQueryable in the dotnetPosts is inlined, resulting in a single database round
trip:

SQL

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].


[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Aggregate functions over subqueries and aggregates on


SQL Server
EF9 improves the translation of some complex queries using aggregate functions
composed over subqueries or other aggregate functions. Below is an example of such
query:

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#

var topRatedPostsAverageRatingByLanguage = await context.Blogs.


Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();

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"

Queries using Count != 0 are optimized

 Tip

The code shown here comes from QuerySample.cs .

In EF8, the following LINQ query was translated to use the SQL COUNT function:

C#

var blogsWithPost = await context.Blogs


.Where(b => b.Posts.Count > 0)
.ToListAsync();

EF9 now generates a more efficient translation using EXISTS :

SQL

SELECT "b"."Id", "b"."Name", "b"."SiteUri"


FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")

C# semantics for comparison operations on nullable


values
In EF8 comparisons between nullable elements were not performed correctly for some
scenarios. In C#, if one or both operands are null, the result of a comparison operation is
false; otherwise, the contained values of operands are compared. In EF8 we used to
translate comparisons using database null semantics. This would produce results
different than similar query using LINQ to Objects. Moreover, we would produce
different results when comparison was done in filter vs projection. Some queries would
also produce different results between Sql Server and Sqlite/Postgres.

For example, the query:

C#

var negatedNullableComparisonFilter = await context.Entities


.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();

would generate the following SQL:

SQL

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]


FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])

which filters out entities whose NullableIntOne or NullableIntTwo are set to null.

In EF9 we produce:
SQL

SELECT [e].[NullableIntOne], [e].[NullableIntTwo]


FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)

Similar comparison performed in a projection:

C#

var negatedNullableComparisonProjection = await context.Entities.Select(x =>


new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();

resulted in the following SQL:

SQL

SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE


WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS
bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]

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

SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne"


> "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"

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!

Translation of Order and OrderDescending LINQ operators


EF9 enables the translation of LINQ simplified ordering operations ( Order and
OrderDescending ). These work similar to OrderBy / OrderByDescending but don't require

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#

var orderOperation = await context.Blogs


.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();

This query is equivalent to the following:

C#

var orderByEquivalent = await context.Blogs


.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx =>
xx).ToList()
})
.ToListAsync();

and produces the following SQL:

SQL

SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].


[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].
[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].
[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]

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!

Improved translation of logical negation operator (!)


EF9 brings many optimizations around SQL CASE/WHEN , COALESCE , negation, and various
other constructs; most of these were contributed by Andrea Canciani (@ranma42 )-
many thanks for all of these! Below, we'll detail just a few of these optimizations around
logical negation.

Let's examine the following query:

C#

var negatedContainsSimplification = await context.Posts


.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();

In EF8 we would produce the following SQL:

SQL

SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)

In EF9 we "push" NOT operation into the comparison:

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#

var caseSimplification = await context.Blogs


.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();

In EF8 used to result in nested CASE blocks:

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]

In EF9 we removed the nesting:

SQL

SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]

On SQL Server, when projecting a negated bool property:

C#

var negatedBoolProjection = await context.Posts.Select(x => new { x.Title,


Active = !x.Archived }).ToListAsync();

EF8 would generate a CASE block because comparisons can't appear in the projection
directly in SQL Server queries:

SQL

SELECT [p].[Title], CASE


WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]

In EF9, this translation has been simplified and now uses bitwise NOT ( ~ ):

SQL

SELECT [p].[Title], ~[p].[Archived] AS [Active]


FROM [Posts] AS [p]

Better support for Azure SQL and Azure Synapse


EF9 allows for more flexibility when specifying the type of SQL Server which is being
targeted. Instead of configuring EF with UseSqlServer , you can now specify UseAzureSql
or UseAzureSynapse . This allows EF to produce better SQL when using Azure SQL or
Azure Synapse. EF can take advantage of the database specific features (e.g. dedicated
type for JSON on Azure SQL), or work around its limitations (e.g. ESCAPE clause is not
available when using LIKE on Azure Synapse).

Other query improvements


The primitive collections querying support introduced in EF8 has been extended to
support all ICollection<T> types. Note that this applies only to parameter and
inline collections - primitive collections that are part of entities are still limited to
arrays, lists and in EF9 also read-only arrays/lists.
New ToHashSetAsync functions to return the results of a query as a HashSet
(#30033 , contributed by @wertzui ).
TimeOnly.FromDateTime and FromTimeSpan are now translated on SQL Server

(#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

returns the starting position of the first occurrence of a pattern (#33702 ,


@smnsht ).
Sum and Average now work for decimals on SQLite (#33721 , contributed by
@ranma42 ).
Fixes and optimizations to string.StartsWith and EndsWith (#31482 ).
Convert.To* methods can now accept argument of type object (#33891 ,
contributed by @imangd ).
Exclusive-Or (XOR) operation is now translated on SQL Server (#34071 ,
contributed by @ranma42 ).
Optimizations around nullability for COLLATE and AT TIME ZONE operations
(#34263 , contributed by @ranma42 ).
Optimizations for DISTINCT over IN , EXISTS and set operations (#34381 ,
contributed by @ranma42 ).

The above were only some of the more important query improvements in EF9; see this
issue for a more complete listing.

Migrations

Protection against concurrent migrations


EF9 introduces a locking mechanism to protect against multiple migration executions
happening simultaneously, as that could leave the database in a corrupted state. This
doesn't happen when migrations are deployed to the production environment using
recommended methods, but can happen if migrations are applied at runtime using the
DbContext.Database.Migrate() method. We recommend applying migrations at
deployment, rather than as part of application startup, but that can result in more
complicated application architectures (e.g. when using .NET Aspire projects.

7 Note

If you are using Sqlite database, see potential issues associated with this feature.

Warn when multiple migration operations can't be run


inside a transaction
The majority of operations performed during migrations are protected by a transaction.
This ensures that if for some reason migration fails, the database does not end up in a
corrupted state. However, some operations are not wrapped in a transaction (e.g.
operations on SQL Server memory-optimized tables, or database altering operations like
modifying the database collation). To avoid corrupting the database in case of migration
failure, it is recommended that these operations are performed in isolation using a
separate migration. EF9 now detects a scenario when a migration contains multiple
operations, one of which can't be wrapped in a transaction, and issues a warning.
Improved data seeding
EF9 introduced a convenient way to perform data seeding, that is populating the
database with initial data. DbContextOptionsBuilder now contains UseSeeding and
UseAsyncSeeding methods which get executed when the DbContext is initialized (as part

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.

Here is an example of how these methods can be used:

C#

protected override void OnConfiguring(DbContextOptionsBuilder


optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=
(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;Connec
tRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url ==
"http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com"
});
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b
=> b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com"
});
await context.SaveChangesAsync(cancellationToken);
}
});
More information can be found here.

Other migration improvements


When changing an existing table into a SQL Server temporal table, the migration
code size has been significantly reduced.

Model building

Auto-compiled models

 Tip

The code shown here comes from the NewInEFCore9.CompiledModels sample.

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

dotnet ef dbcontext optimize

After running the command, a line like,


.UseModel(MyCompiledModels.BlogsContextModel.Instance) must be added to

OnConfiguring to tell EF Core to use the compiled model.

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

Build succeeded in 0.3s

Build succeeded in 0.3s


Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered
automatically, but you can also call
'options.UseModel(BlogsContextModel.Instance)'. Run this command again when
the model is modified.
PS
D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.Compile
dModels\Model>

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

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-


preview.4.24205.3
 Tip

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

MSBuild property Description

EFOptimizeContext Set to true to enable auto-compiled models.

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.

In our example, we need to specify the startup project:

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.

Read-only primitive collections

 Tip

The code shown here comes from PrimitiveCollectionsSample.cs .

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#

public class DogWalk


{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

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#

public class Pub


{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }

private List<DateOnly> _daysVisited = new();


public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}

These collections can then be used in queries in the normal way. For example, this LINQ
query:
C#

var walksWithADrink = await context.Walks.Select(


w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v =>
w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();

Which translates to the following SQL on SQLite:

SQL

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (


SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Specify fill-factor for keys and indexes

 Tip

The code shown here comes from ModelBuildingSample.cs .

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

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];


ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region],


[Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH
(FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH
(FILLFACTOR = 80);

This enhancement was contributed by @deano-hunter . Many thanks!

Make existing model building conventions more


extensible

 Tip

The code shown here comes from CustomConventionsSample.cs .

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)
{
}

public override void ProcessEntityTypeAdded(


IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);

public override void ProcessEntityTypeBaseTypeChanged(


IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}

private void Process(IConventionEntityTypeBuilder entityTypeBuilder)


{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute),
inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&&
Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}

IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;

foreach (var property in clrType.GetRuntimeProperties()


.Where(p => p.GetMethod != null &&
!p.GetMethod.IsStatic))
{
yield return property;
}

foreach (var property in clrType.GetRuntimeFields())


{
yield return property;
}
}
}
}

In EF9, this can be simplified down to the following:

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;
}
}

Update ApplyConfigurationsFromAssembly to call non-


public constructors
In previous versions of EF Core, the ApplyConfigurationsFromAssembly method only
instantiated configuration types with a public, parameterless constructors. In EF9, we
have both improved the error messages generated when this fails , and also enabled
instantiation by non-public constructor. This is useful when co-locating configuration in
a private nested class which should never be instantiated by application code. For
example:

C#

public class Country


{
public int Code { get; set; }
public required string Name { get; set; }

private class FooConfiguration : IEntityTypeConfiguration<Country>


{
private FooConfiguration()
{
}

public void Configure(EntityTypeBuilder<Country> builder)


{
builder.HasKey(e => e.Code);
}
}
}

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

SQL Server HierarchyId

 Tip

The code shown here comes from HierarchyIdSample.cs .

Sugar for HierarchyId path generation


First class support for the SQL Server HierarchyId type was added in EF8. In EF9, a sugar
method has been added to make it easier to create new child nodes in the tree
structure. For example, the following code queries for an existing entity with a
HierarchyId property:

C#

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");


This HierarchyId property can then be used to create child nodes without any explicit
string manipulation. For example:

C#

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1),


"Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2),
"Wills");

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#

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5),


"Toast");

This creates a node with a HierarchyId of /4/1/3/1/1.5/ , putting it between child1


and child2 .

This enhancement was contributed by @Rezakazemi890 . Many thanks!

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.

We believe a community contribution from @Suchiman has fixed this. However,


we're also conscious that tweaks around MSBuild behaviors have a tendency to have
unintended consequences, so we're asking people like you to try this out and report
back on any negative experiences you have.
What's new in .NET 8
Article • 02/08/2024

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

Windows Presentation Foundation


Windows Presentation Foundation (WPF) adds the ability to use hardware acceleration
and a new OpenFolderDialog control. For more information, see What's new in WPF for
.NET 8.

See also
Breaking changes in .NET 8

.NET preview announcements


Announcing .NET 8
Announcing .NET 8 RC 2
Announcing .NET 8 RC 1
Announcing .NET 8 Preview 7
Announcing .NET 8 Preview 6
Announcing .NET 8 Preview 5
Announcing .NET 8 Preview 4
Announcing .NET 8 Preview 3
Announcing .NET 8 Preview 2
Announcing .NET 8 Preview 1

ASP.NET Core preview announcements


ASP.NET Core in .NET 8
ASP.NET Core updates in .NET 8 RC 2
ASP.NET Core updates in .NET 8 RC 1
ASP.NET Core updates in .NET 8 Preview 7
ASP.NET Core updates in .NET 8 Preview 6
ASP.NET Core updates in .NET 8 Preview 5
ASP.NET Core updates in .NET 8 Preview 4
ASP.NET Core updates in .NET 8 Preview 3
ASP.NET Core updates in .NET 8 Preview 2
ASP.NET Core updates in .NET 8 Preview 1
What's new in ASP.NET Core 8.0
Article • 11/06/2024

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.

Interactive render modes also prerender content by default.

For more information, see the following articles:

ASP.NET Core Blazor fundamentals: New sections on rendering and


static/interactive concepts appear at the top of the article.
ASP.NET Core Blazor render modes
Migration coverage: Migrate from ASP.NET Core 7.0 to 8.0

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.

New article on class libraries with static server-side


rendering (static SSR)
We've added a new article that discusses component library authorship in Razor class
libraries (RCLs) with static server-side rendering (static SSR).

For more information, see ASP.NET Core Razor class libraries (RCLs) with static server-
side rendering (static SSR).

New article on HTTP caching issues


We've added a new article that discusses some of the common HTTP caching issues that
can occur when upgrading Blazor apps across major versions and how to address HTTP
caching issues.

For more information, see Avoid HTTP caching issues when upgrading ASP.NET Core
Blazor apps.

New Blazor Web App template


We've introduced a new Blazor project template: the Blazor Web App template. The new
template provides a single starting point for using Blazor components to build any style
of web UI. The template combines the strengths of the existing Blazor Server and Blazor
WebAssembly hosting models with the new Blazor capabilities added in .NET 8: static
server-side rendering (static SSR), streaming rendering, enhanced navigation and form
handling, and the ability to add interactivity using either Blazor Server or Blazor
WebAssembly on a per-component basis.

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:

Tooling for ASP.NET Core Blazor


ASP.NET Core Blazor project structure
New JS initializers for Blazor Web Apps
For Blazor Server, Blazor WebAssembly, and Blazor Hybrid apps:

beforeStart is used for tasks such as customizing the loading process, logging

level, and other options.


afterStarted is used for tasks such as registering Blazor event listeners and

custom event types.

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 .

For more information, see ASP.NET Core Blazor startup.

Split of prerendering and integration guidance


For prior releases of .NET, we covered prerendering and integration in a single article. To
simplify and focus our coverage, we've split the subjects into the following new articles,
which have been updated for .NET 8:

Prerender ASP.NET Core Razor components


Integrate ASP.NET Core Razor components into ASP.NET Core apps

Persist component state in a Blazor Web App


You can persist and read component state in a Blazor Web App using the existing
PersistentComponentState service. This is useful for persisting component state during
prerendering.

Blazor Web Apps automatically persist any registered app-level state created during
prerendering, removing the need for the Persist Component State Tag Helper.

Form handling and model binding


Blazor components can now handle submitted form requests, including model binding
and validating the request data. Components can implement forms with separate form
handlers using the standard HTML <form> tag or using the existing EditForm
component.
Form model binding in Blazor honors the data contract attributes (for example,
[DataMember] and [IgnoreDataMember] ) for customizing how the form data is bound to

the model.

New antiforgery support is included in .NET 8. A new AntiforgeryToken component


renders an antiforgery token as a hidden field, and the new [RequireAntiforgeryToken]
attribute enables antiforgery protection. If an antiforgery check fails, a 400 (Bad Request)
response is returned without form processing. The new antiforgery features are enabled
by default for forms based on Editform and can be applied manually to standard HTML
forms.

For more information, see ASP.NET Core Blazor forms overview.

Enhanced navigation and form handling


Static server-side rendering (static SSR) typically performs a full page refresh whenever
the user navigates to a new page or submits a form. In .NET 8, Blazor can enhance page
navigation and form handling by intercepting the request and performing a fetch
request instead. Blazor then handles the rendered response content by patching it into
the browser DOM. Enhanced navigation and form handling avoids the need for a full
page refresh and preserves more of the page state, so pages load faster and more
smoothly. Enhanced navigation is enabled by default when the Blazor script
( blazor.web.js ) is loaded. Enhanced form handling can be optionally enabled for
specific forms.

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:

Enhanced navigation and form handling


Location changes

New article on static rendering with enhanced navigation


for JS interop
Some apps depend on JS interop to perform initialization tasks that are specific to each
page. When using Blazor's enhanced navigation feature with statically-rendered pages
that perform JS interop initialization tasks, page-specific JS may not be executed again
as expected each time an enhanced page navigation occurs. A new article explains how
to address this scenario in Blazor Web Apps:
ASP.NET Core Blazor JavaScript with static server-side rendering (static SSR)

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.

For more information, see ASP.NET Core Razor component rendering.

Inject keyed services into components


Blazor now supports injecting keyed services using the [Inject] attribute. Keys allow for
scoping of registration and consumption of services when using dependency injection.
Use the new InjectAttribute.Key property to specify the key for the service to inject:

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.

For more information, see ASP.NET Core Blazor dependency injection.

Access HttpContext as a cascading parameter


You can now access the current HttpContext as a cascading parameter from a static
server component:
C#

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

Render Razor components outside of ASP.NET Core


You can now render Razor components outside the context of an HTTP request. You can
render Razor components as HTML directly to a string or stream independently of the
ASP.NET Core hosting environment. This is convenient for scenarios where you want to
generate HTML fragments, such as for a generating email or static site content.

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.

For more information, see ASP.NET Core Blazor sections.

Error page support


Blazor Web Apps can define a custom error page for use with the ASP.NET Core
exception handling middleware. The Blazor Web App project template includes a default
error page ( Components/Pages/Error.razor ) with similar content to the one used in MVC
and Razor Pages apps. When the error page is rendered in response to a request from
Exception Handling Middleware, the error page always renders as a static server
component, even if interactivity is otherwise enabled.

Error.razor in 8.0 reference source

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

Route to named elements


Blazor now supports using client-side routing to navigate to a specific HTML element on
a page using standard URL fragments. If you specify an identifier for an HTML element
using the standard id attribute, Blazor correctly scrolls to that element when the URL
fragment matches the element identifier.

For more information, see ASP.NET Core Blazor routing and navigation.

Root-level cascading values


Root-level cascading values can be registered for the entire component hierarchy.
Named cascading values and subscriptions for update notifications are supported.

For more information, see ASP.NET Core Blazor cascading values and parameters.

Virtualize empty content


Use the new EmptyContent parameter on the Virtualize component to supply content
when the component has loaded and either Items is empty or
ItemsProviderResult<T>.TotalItemCount is zero.

For more information, see ASP.NET Core Razor component virtualization.

Close circuits when there are no remaining interactive


server components
Interactive server components handle web UI events using a real-time connection with
the browser called a circuit. A circuit and its associated state are set up when a root
interactive server component is rendered. The circuit is closed when there are no
remaining interactive server components on the page, which frees up server resources.
Monitor SignalR circuit activity
You can now monitor inbound circuit activity in server-side apps using the new
CreateInboundActivityHandler method on CircuitHandler . Inbound circuit activity is

any activity sent from the browser to the server, such as UI events or JavaScript-to-.NET
interop calls.

For more information, see ASP.NET Core Blazor SignalR guidance.

Faster runtime performance with the Jiterpreter


The Jiterpreter is a new runtime feature in .NET 8 that enables partial Just-in-Time (JIT)
compilation support when running on WebAssembly to achieve improved runtime
performance.

For more information, see Host and deploy ASP.NET Core Blazor WebAssembly.

Ahead-of-time (AOT) SIMD and exception handling


Blazor WebAssembly ahead-of-time (AOT) compilation now uses WebAssembly Fixed-
width SIMD and WebAssembly Exception handling by default to improve runtime
performance.

For more information, see the following articles:

AOT: Single Instruction, Multiple Data (SIMD)


AOT: Exception handling

Web-friendly Webcil packaging


Webcil is web-friendly packaging of .NET assemblies that removes content specific to
native Windows execution to avoid issues when deploying to environments that block
the download or use of .dll files. Webcil is enabled by default for Blazor WebAssembly
apps.

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.

Blazor WebAssembly debugging improvements


When debugging .NET on WebAssembly, the debugger now downloads symbol data
from symbol locations that are configured in Visual Studio preferences. This improves
the debugging experience for apps that use NuGet packages.

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 Debug ASP.NET Core Blazor apps.

Content Security Policy (CSP) compatibility


Blazor WebAssembly no longer requires enabling the unsafe-eval script source when
specifying a Content Security Policy (CSP).

For more information, see Enforce a Content Security Policy for ASP.NET Core Blazor.

Handle caught exceptions outside of a Razor


component's lifecycle
Use ComponentBase.DispatchExceptionAsync in a Razor component to process exceptions
thrown outside of the component's lifecycle call stack. This permits the component's
code to treat exceptions as though they're lifecycle method exceptions. Thereafter,
Blazor's error handling mechanisms, such as error boundaries, can process exceptions.

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.

For more information, see ASP.NET Core Blazor startup.

Configuration of connection timeouts in


HubConnectionBuilder

Prior workarounds for configuring hub connection timeouts can be replaced with formal
SignalR hub connection builder timeout configuration.

For more information, see the following:

ASP.NET Core Blazor SignalR guidance


Host and deploy ASP.NET Core Blazor WebAssembly
Host and deploy ASP.NET Core server-side Blazor apps

Project templates shed Open Iconic


The Blazor project templates no longer depend on Open Iconic for icons.

Support for dialog cancel and close events


Blazor now supports the cancel and close events on the dialog HTML element.

In the following example:

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>

<dialog id="my-dialog" @onclose="OnClose" @oncancel="OnCancel">


<p>Hi there!</p>
<form method="dialog">
<button>Close</button>
</form>
</dialog>
</div>

@code {
private string? message;

private void OnClose(EventArgs e) => message += "onclose, ";

private void OnCancel(EventArgs e) => message += "oncancel, ";


}

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 the following resources:

Secure ASP.NET Core server-side Blazor apps


What's new with identity in .NET 8 (blog post)

Secure Blazor WebAssembly with ASP.NET Core Identity


The Blazor documentation hosts a new article and sample app to cover securing a
standalone Blazor WebAssembly app with ASP.NET Core Identity.

For more information, see the following resources:

Secure ASP.NET Core Blazor WebAssembly with ASP.NET Core Identity


What's new with identity in .NET 8 (blog post)

Blazor Server with Yarp routing


Routing and deep linking for Blazor Server with Yarp work correctly in .NET 8.

For more information, see Migrate from ASP.NET Core 7.0 to 8.0.

Multiple Blazor Web Apps per server project


Support for multiple Blazor Web Apps per server project will be considered for .NET 10
(November, 2025).

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.

[Parameter] attribute is no longer required when


supplied from the query string
The [Parameter] attribute is no longer required when supplying a parameter from the
query string:

diff

- [Parameter]
[SupplyParameterFromQuery]

SignalR

New approach to set the server timeout and Keep-Alive


interval
ServerTimeout (default: 30 seconds) and KeepAliveInterval (default: 15 seconds) can be
set directly on HubConnectionBuilder.

Prior approach for JavaScript clients

The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:

JavaScript

var connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.build();

connection.serverTimeoutInMilliseconds = 60000;
connection.keepAliveIntervalInMilliseconds = 30000;

New approach for JavaScript clients


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later:

JavaScript

var connection = new signalR.HubConnectionBuilder()


.withUrl("/chatHub")
.withServerTimeout(60000)
.withKeepAlive(30000)
.build();

Prior approach for the JavaScript client of a Blazor Server app

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

New approach for the JavaScript client of server-side Blazor app


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later for Blazor Web Apps and Blazor Server.

Blazor Web App:

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

Prior approach for .NET clients


The following example shows the assignment of values that are double the default
values in ASP.NET Core 7.0 or earlier:

C#

var builder = new HubConnectionBuilder()


.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.Build();

builder.ServerTimeout = TimeSpan.FromSeconds(60);
builder.KeepAliveInterval = TimeSpan.FromSeconds(30);

builder.On<string, string>("ReceiveMessage", (user, message) => ...


await builder.StartAsync();

New approach for .NET clients


The following example shows the new approach for assigning values that are double the
default values in ASP.NET Core 8.0 or later:

C#

var builder = new HubConnectionBuilder()


.WithUrl(Navigation.ToAbsoluteUri("/chathub"))
.WithServerTimeout(TimeSpan.FromSeconds(60))
.WithKeepAliveInterval(TimeSpan.FromSeconds(30))
.Build();

builder.On<string, string>("ReceiveMessage", (user, message) => ...

await builder.StartAsync();

SignalR stateful reconnect


SignalR stateful reconnect reduces the perceived downtime of clients that have a
temporary disconnect in their network connection, such as when switching network
connections or a short temporary loss in access.

Stateful reconnect achieves this by:

Temporarily buffering data on the server and client.


Acknowledging messages received (ACK-ing) by both the server and client.
Recognizing when a connection is returning and replaying messages that might
have been sent while the connection was down.

Stateful reconnect is available in ASP.NET Core 8.0 and later.

Opt in to stateful reconnect at both the server hub endpoint and the client:

Update the server hub endpoint configuration to enable the


AllowStatefulReconnects option:

C#

app.MapHub<MyHub>("/hubName", options =>


{
options.AllowStatefulReconnects = true;
});

Optionally, the maximum buffer size in bytes allowed by the server can be set
globally or for a specific hub with the StatefulReconnectBufferSize option:

The StatefulReconnectBufferSize option set globally:

C#

builder.AddSignalR(o => o.StatefulReconnectBufferSize = 1000);

The StatefulReconnectBufferSize option set for a specific hub:

C#

builder.AddSignalR().AddHubOptions<MyHub>(o =>
o.StatefulReconnectBufferSize = 1000);

The StatefulReconnectBufferSize option is optional with a default of 100,000


bytes.

Update JavaScript or TypeScript client code to enable the withStatefulReconnect


option:

JavaScript

const builder = new signalR.HubConnectionBuilder()


.withUrl("/hubname")
.withStatefulReconnect({ bufferSize: 1000 }); // Optional, defaults
to 100,000
const connection = builder.build();

The bufferSize option is optional with a default of 100,000 bytes.

Update .NET client code to enable the WithStatefulReconnect option:

C#

var builder = new HubConnectionBuilder()


.WithUrl("<hub url>")
.WithStatefulReconnect();
builder.Services.Configure<HubConnectionOptions>(o =>
o.StatefulReconnectBufferSize = 1000);
var hubConnection = builder.Build();
The StatefulReconnectBufferSize option is optional with a default of 100,000
bytes.

For more information, see Configure stateful reconnect.

Minimal APIs
This section describes new features for minimal APIs. See also the section on Native AOT
for more information relevant to minimal APIs.

User override culture


Starting in ASP.NET Core 8.0, the
RequestLocalizationOptions.CultureInfoUseUserOverride property allows the application
to decide whether or not to use nondefault Windows settings for the CultureInfo
DateTimeFormat and NumberFormat properties. This has no impact on Linux. This
directly corresponds to UseUserOverride.

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.

Inferred binding to forms using the IFormCollection, IFormFile, and IFormFileCollection


types is also supported. OpenAPI metadata is inferred for form parameters to support
integration with Swagger UI.

For more information, see:

Explicit binding from form values.


Binding to forms with IFormCollection, IFormFile, and IFormFileCollection.
Form binding in minimal APIs

Binding from forms is now supported for:


Collections, for example List and Dictionary
Complex types, for example, Todo or Project

For more information, see Bind to collections and complex types from forms.

Antiforgery with minimal APIs


This release adds a middleware for validating antiforgery tokens, which are used to
mitigate cross-site request forgery attacks. Call AddAntiforgery to register antiforgery
services in DI. WebApplicationBuilder automatically adds the middleware when the
antiforgery services have been registered in the DI container. Antiforgery tokens are
used to mitigate cross-site request forgery attacks.

C#

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", () => "Hello World!");

app.Run();

The antiforgery middleware:

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 antiforgery token is only validated if:

The endpoint contains metadata implementing IAntiforgeryMetadata where


RequiresValidation=true .

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.

For more information, see Antiforgery with Minimal APIs.

New IResettable interface in ObjectPool


Microsoft.Extensions.ObjectPool provides support for pooling object instances in
memory. Apps can use an object pool if the values are expensive to allocate or initialize.

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.

For more information, see the ObjectPool sample.

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 and Native AOT


Many of the popular libraries used in ASP.NET Core projects currently have some
compatibility issues when used in a project targeting Native AOT, such as:

Use of reflection to inspect and discover types.


Conditionally loading libraries at runtime.
Generating code on the fly to implement functionality.

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.

Library authors hoping to support Native AOT are encouraged to:

Read about Native AOT compatibility requirements.


Prepare the library for trimming.

New project template


The new ASP.NET Core Web API (Native AOT) project template (short name webapiaot )
creates a project with AOT publish enabled. For more information, see The Web API
(Native AOT) template.
New CreateSlimBuilder method
The CreateSlimBuilder() method used in the Web API (Native AOT) template initializes
the WebApplicationBuilder with the minimum ASP.NET Core features necessary to run
an app. The CreateSlimBuilder method includes the following features that are typically
needed for an efficient development experience:

JSON file configuration for appsettings.json and appsettings.


{EnvironmentName}.json .

User secrets configuration.


Console logging.
Logging configuration.

For more information, see The CreateSlimBuilder method.

New CreateEmptyBuilder method


There's another new WebApplicationBuilder factory method for building small apps that
only contain necessary features:
WebApplication.CreateEmptyBuilder(WebApplicationOptions options) . This

WebApplicationBuilder is created with no built-in behavior. The app it builds contains

only the services and middleware that are explicitly configured.

Here’s an example of using this API to create a small web application:

C#

var builder = WebApplication.CreateEmptyBuilder(new


WebApplicationOptions());
builder.WebHost.UseKestrelCore();

var app = builder.Build();

app.Use(async (context, next) =>


{
await context.Response.WriteAsync("Hello, World!");
await next(context);
});

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

added by calling builder.WebHost.UseKestrelHttpsConfiguration() for HTTPS or


builder.WebHost.UseQuic() for HTTP/3. For more information, see The CreateSlimBuilder
method.

JSON serialization of compiler-generated


IAsyncEnumerable<T> types

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.

One of the new features is support for JSON serialization of IAsyncEnumerable<T>


implementations implemented by the C# compiler. This support opens up their use in
ASP.NET Core projects configured to publish Native 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.

For information abut other improvements in System.Text.Json source generation, see


Serialization improvements in .NET 8.

Top-level APIs annotated for trim warnings


The main entry points to subsystems that don't work reliably with Native AOT are now
annotated. When these methods are called from an application with Native AOT
enabled, a warning is provided. For example, the following code produces a warning at
the invocation of AddControllers because this API isn't trim-safe and isn't supported by
Native AOT.
Request delegate generator
In order to make Minimal APIs compatible with Native AOT, we're introducing the
Request Delegate Generator (RDG). The RDG is a source generator that does what the
RequestDelegateFactory (RDF) does. That is, it turns the various MapGet() , MapPost() ,
and calls like them into RequestDelegate instances associated with the specified routes.
But rather than doing it in-memory in an application when it starts, the RDG does it at
compile time and generates C# code directly into the project. The RDG:

Removes the runtime generation of this code.


Ensures that the types used in APIs are statically analyzable by the Native AOT
tool-chain.
Ensures that required code isn't trimmed away.

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.

Improved performance using Interceptors


The Request Delegate Generator uses the new C# 12 interceptors compiler feature to
support intercepting calls to minimal API Map methods with statically generated
variants at runtime. The use of interceptors results in increased startup performance for
apps compiled with PublishAot .

Logging and exception handling in compile-time


generated minimal APIs
Minimal APIs generated at run time support automatically logging (or throwing
exceptions in Development environments) when parameter binding fails. .NET 8
introduces the same support for APIs generated at compile time via the Request
Delegate Generator (RDG). For more information, see Logging and exception handling
in compile-time generated minimal APIs .

AOT and System.Text.Json


Minimal APIs are optimized for receiving and returning JSON payloads using
System.Text.Json , so the compatibility requirements for JSON and Native AOT apply

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#

// Register the JSON serializer context with DI


builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0,
AppJsonSerializerContext.Default);
});

...

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

Libraries and Native AOT


Many of the common libraries available for ASP.NET Core projects today have some
compatibility issues if used in a project targeting Native AOT. Popular libraries often rely
on the dynamic capabilities of .NET reflection to inspect and discover types,
conditionally load libraries at runtime, and generate code on the fly to implement their
functionality. These libraries need to be updated in order to work with Native AOT by
using tools like Roslyn source generators.

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.

Kestrel and HTTP.sys servers


There are several new features for Kestrel and HTTP.sys.

Support for named pipes in Kestrel


Named pipes is a popular technology for building inter-process communication (IPC)
between Windows apps. You can now build an IPC server using .NET, Kestrel, and named
pipes.

C#

var builder = WebApplication.CreateBuilder(args);


builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ListenNamedPipe("MyPipeName");
});

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.

Performance improvements to named pipes transport


We’ve improved named pipe connection performance. Kestrel’s named pipe transport
now accepts connections in parallel, and reuses NamedPipeServerStream instances.

Time to create 100,000 connections:


Before : 5.916 seconds
After : 2.374 seconds

HTTP/2 over TLS (HTTPS) support on macOS in Kestrel


.NET 8 adds support for Application-Layer Protocol Negotiation (ALPN) to macOS. ALPN
is a TLS feature used to negotiate which HTTP protocol a connection will use. For
example, ALPN allows browsers and other HTTP clients to request an HTTP/2
connection. This feature is especially useful for gRPC apps, which require HTTP/2. For
more information, see Use HTTP/2 with the ASP.NET Core Kestrel web server.

Certificate file watching in Kestrel


TLS certificates configured by path are now monitored for changes when
reloadOnChange is passed to KestrelServerOptions.Configure(). A change to the

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.

Warning when specified HTTP protocols won't be used


If TLS is disabled and HTTP/1.x is available, HTTP/2 and HTTP/3 will be disabled, even if
they've been specified. This can cause some nasty surprises, so we've added warning
output to let you know when it happens.

HTTP_PORTS and HTTPS_PORTS config keys

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 host name in ITlsHandshakeFeature


The Server Name Indication (SNI) host name is now exposed in the HostName
property of the ITlsHandshakeFeature interface.

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.

For more information, see ITlsHandshakeFeature.HostName .

IHttpSysRequestTimingFeature
IHttpSysRequestTimingFeature provides detailed timing information for requests
when using the HTTP.sys server and In-process hosting with IIS:

Timestamps are obtained using QueryPerformanceCounter.


The timestamp frequency can be obtained via QueryPerformanceFrequency.
The index of the timing can be cast to HttpSysRequestTimingType to know what
the timing represents.
The value might be 0 if the timing isn't available for the current request.
IHttpSysRequestTimingFeature.TryGetTimestamp retrieves the timestamp for the
provided timing type:

C#

using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Server.HttpSys;
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseHttpSys();

var app = builder.Build();

app.Use((context, next) =>


{
var feature =
context.Features.GetRequiredFeature<IHttpSysRequestTimingFeature>();

var loggerFactory =
context.RequestServices.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger("Sample");

var timingType = HttpSysRequestTimingType.RequestRoutingEnd;

if (feature.TryGetTimestamp(timingType, out var timestamp))


{
logger.LogInformation("Timestamp {timingType}: {timestamp}",
timingType, timestamp);
}
else
{
logger.LogInformation("Timestamp {timingType}: not available for the
"
+ "current request",
timingType);
}

return next(context);
});

app.MapGet("/", () => Results.Ok());

app.Run();

For more information, see Get detailed timing information with


IHttpSysRequestTimingFeature and Timing information and In-process hosting with IIS.

HTTP.sys: opt-in support for kernel-mode response


buffering
In some scenarios, high volumes of small writes with high latency can cause significant
performance impact to HTTP.sys . This impact is due to the lack of a Pipe buffer in the
HTTP.sys implementation. To improve performance in these scenarios, support for

response buffering has been added to HTTP.sys . Enable buffering by setting


HttpSysOptions.EnableKernelResponseBuffering to true .

Response buffering should be enabled by an app that does synchronous I/O, or


asynchronous I/O with no more than one outstanding write at a time. In these scenarios,
response buffering can significantly improve throughput over high-latency connections.

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.

Authentication and authorization


ASP.NET Core 8 adds new features to authentication and authorization.

Identity API endpoints


MapIdentityApi<TUser> is a new extension method that adds two API endpoints
( /register and /login ). The main goal of the MapIdentityApi is to make it easy for
developers to use ASP.NET Core Identity for authentication in JavaScript-based single
page apps (SPA) or Blazor apps. Instead of using the default UI provided by ASP.NET
Core Identity, which is based on Razor Pages, MapIdentityApi adds JSON API endpoints
that are more suitable for SPA apps and nonbrowser apps. For more information, see
Identity API endpoints .

IAuthorizationRequirementData
Prior to ASP.NET Core 8, adding a parameterized authorization policy to an endpoint
required implementing an:

AuthorizeAttribute for each policy.

AuthorizationPolicyProvider to process a custom policy from a string-based

contract.
AuthorizationRequirement for the policy.

AuthorizationHandler for each requirement.

For example, consider the following sample written for ASP.NET Core 7.0:
C#

using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();

var app = builder.Build();

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;
}

// Check whether a given MinimumAgeRequirement is satisfied or not for a


particular
// context.
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement
requirement)
{
// Log as a warning so that it's very clear in sample output which
authorization
// policies(and requirements/handlers) are in use.
_logger.LogWarning("Evaluating authorization requirement for age >=
{age}",

requirement.Age);

// Check the user's age


var dateOfBirthClaim = context.User.FindFirst(c => c.Type ==

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--;
}

// If the user meets the age criterion, mark the authorization


requirement
// succeeded.
if (age >= requirement.Age)
{
_logger.LogInformation("Minimum age authorization
requirement {age} satisfied",
requirement.Age);
context.Succeed(requirement);
}
else
{
_logger.LogInformation("Current user's DateOfBirth claim
({dateOfBirth})" +
" does not satisfy the minimum age authorization
requirement {age}",
dateOfBirthClaim.Value,
requirement.Age);
}
}
else
{
_logger.LogInformation("No DateOfBirth claim present");
}

return Task.CompletedTask;
}
}

The complete sample is here in the AspNetCore.Docs.Samples repository.

ASP.NET Core 8 introduces the IAuthorizationRequirementData interface. The


IAuthorizationRequirementData interface allows the attribute definition to specify the

requirements associated with the authorization policy. Using


IAuthorizationRequirementData , the preceding custom authorization policy code can be

written with fewer lines of code. The updated Program.cs file:

diff

using AuthRequirementsData.Authorization;
using Microsoft.AspNetCore.Authorization;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();
- builder.Services.AddSingleton<IAuthorizationPolicyProvider,
MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler,
MinimumAgeAuthorizationHandler>();

var app = builder.Build();

app.MapControllers();

app.Run();

The updated MinimumAgeAuthorizationHandler :

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;
}

// Check whether a given MinimumAgeRequirement is satisfied or not for a


particular
// context
protected override Task
HandleRequirementAsync(AuthorizationHandlerContext context,
- MinimumAgeRequirement
requirement)
+ MinimumAgeAuthorizeAttribute
requirement)
{
// Remaining code omitted for brevity.

The complete updated sample can be found here .

See Custom authorization policies with IAuthorizationRequirementData for a detailed


examination of the new sample.

Securing Swagger UI endpoints


Swagger UI endpoints can now be secured in production environments by calling
MapSwagger().RequireAuthorization. For more information, see Securing Swagger UI
endpoints

Miscellaneous
The following sections describe miscellaneous new features in ASP.NET Core 8.

Keyed services support in Dependency Injection


Keyed services refers to a mechanism for registering and retrieving Dependency Injection
(DI) services using keys. A service is associated with a key by calling AddKeyedSingleton
(or AddKeyedScoped or AddKeyedTransient ) to register it. Access a registered service by
specifying the key with the [FromKeyedServices] attribute. The following code shows
how to use keyed services:

C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) =>


bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>

smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache


{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache


{
public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache
cache)
{
return cache.Get("data-mvc");
}
}

public class MyHub : Hub


{
public void Method([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}

Visual Studio project templates for SPA apps with


ASP.NET Core backend
Visual Studio project templates are now the recommended way to create single-page
apps (SPAs) that have an ASP.NET Core backend. Templates are provided that create
apps based on the JavaScript frameworks Angular , React , and Vue . These
templates:

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

Support for generic attributes


Attributes that previously required a Type parameter are now available in cleaner
generic variants. This is made possible by support for generic attributes in C# 11. For
example, the syntax for annotating the response type of an action can be modified as
follows:

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

Generic variants are supported for the following attributes:

[ProducesResponseType<T>]
[Produces<T>]
[MiddlewareFilter<T>]

[ModelBinder<T>]
[ModelMetadataType<T>]

[ServiceFilter<T>]

[TypeFilter<T>]

Code analysis in ASP.NET Core apps


The new analyzers shown in the following table are available in ASP.NET Core 8.0.

ノ Expand table

Diagnostic Breaking or Description


ID nonbreaking

ASP0016 Nonbreaking Don't return a value from RequestDelegate

ASP0019 Nonbreaking Suggest using IHeaderDictionary.Append or the indexer

ASP0020 Nonbreaking Complex types referenced by route parameters must be


parsable

ASP0021 Nonbreaking The return type of the BindAsync method must be


ValueTask<T>

ASP0022 Nonbreaking Route conflict detected between route handlers

ASP0023 Nonbreaking MVC: Route conflict detected between route handlers

ASP0024 Nonbreaking Route handler has multiple parameters with the


[FromBody] attribute

ASP0025 Nonbreaking Use AddAuthorizationBuilder

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:

Route syntax highlighting


Autocomplete of parameter and route names
Autocomplete of route constraints
Route analyzers and fixers
Route syntax analyzer
Mismatched parameter optionality analyzer and fixer
Ambiguous Minimal API and Web API route analyzer
Support for Minimal APIs, Web APIs, and Blazor

For more information, see Route tooling in .NET 8 .

ASP.NET Core metrics


Metrics are measurements reported over time and are most often used to monitor the
health of an app and to generate alerts. For example, a counter that reports failed HTTP
requests could be displayed in dashboards or generate alerts when failures pass a
threshold.

This preview adds new metrics throughout ASP.NET Core using


System.Diagnostics.Metrics. Metrics is a modern API for reporting and collecting
information about apps.

Metrics offers many improvements compared to existing event counters:

New kinds of measurements with counters, gauges and histograms.


Powerful reporting with multi-dimensional values.
Integration into the wider cloud native ecosystem by aligning with OpenTelemetry
standards.

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.

IExceptionHandler implementations are registered by calling

IServiceCollection.AddExceptionHandler<T> . Multiple implementations can be added,


and they're called in the order registered. If an exception handler handles a request, it
can return true to stop processing. If an exception isn't handled by any exception
handler, then control falls back to the default behavior and options from the
middleware.

For more information, see IExceptionHandler.


Improved debugging experience
Debug customization attributes have been added to types like HttpContext ,
HttpRequest , HttpResponse , ClaimsPrincipal , and WebApplication . The enhanced

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:

The debugger display for WebApplication highlights important information such as


configured endpoints, middleware, and IConfiguration values.

.NET 7:
.NET 8:

For more information about debugging improvements in .NET 8, see:

Debugging Enhancements in .NET 8


GitHub issue dotnet/aspnetcore 48205

IPNetwork.Parse and TryParse

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

Here are IPv4 examples:

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);

And here are examples for IPv6:

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);

Redis-based output caching


ASP.NET Core 8 adds support for using Redis as a distributed cache for output caching.
Output caching is a feature that enables an app to cache the output of a minimal API
endpoint, controller action, or Razor Page. For more information, see Output caching.

Short-circuit middleware after routing


When routing matches an endpoint, it typically lets the rest of the middleware pipeline
run before invoking the endpoint logic. Services can reduce resource usage by filtering
out known requests early in the pipeline. Use the ShortCircuit extension method to
cause routing to invoke the endpoint logic immediately and then end the request. For
example, a given route might not need to go through authentication or CORS
middleware. The following example short-circuits requests that match the /short-
circuit route:

C#

app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();

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#

app.MapShortCircuit(404, "robots.txt", "favicon.ico");

For more information, see Short-circuit middleware after routing.


HTTP logging middleware extensibility
The HTTP logging middleware has several new capabilities:

HttpLoggingFields.Duration: When enabled, the middleware emits a new log at the


end of the request and response that measures the total time taken for processing.
This new field has been added to the HttpLoggingFields.All set.
HttpLoggingOptions.CombineLogs: When enabled, the middleware consolidates
all of its enabled logs for a request and response into one log at the end. A single
log message includes the request, request body, response, response body, and
duration.
IHttpLoggingInterceptor: A new interface for a service that can be implemented
and registered (using AddHttpLoggingInterceptor) to receive per-request and per-
response callbacks for customizing what details get logged. Any endpoint-specific
log settings are applied first and can then be overridden in these callbacks. An
implementation can:
Inspect a request and response.
Enable or disable any HttpLoggingFields.
Adjust how much of the request or response body is logged.
Add custom fields to the logs.

For more information, see HTTP logging in .NET Core and ASP.NET Core.

New APIs in ProblemDetails to support more resilient


integrations
In .NET 7, the ProblemDetails service was introduced to improve the experience for
generating error responses that comply with the ProblemDetails specification . In .NET
8, a new API was added to make it easier to implement fallback behavior if
IProblemDetailsService isn't able to generate ProblemDetails. The following example
illustrates use of the new TryWriteAsync API:

C#

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddProblemDetails();

var app = builder.Build();

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.MapGet("/", () => "Test by calling /exception");

app.Run();

For more information, see IProblemDetailsService fallback

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.

Data binding improvements


A new data binding engine was in preview with .NET 7, and is now fully enabled in .NET
8. Though not as extensive as the existing Windows Forms data binding engine, this new
engine is modeled after WPF, which makes it easier to implement MVVM design
principles.

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.

An optional parameter can be provided when the command is invoked, by the


specifying a value for the button's CommandParameter property.

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.

Visual Studio DPI improvements


Visual Studio 2022 17.8 Introduces DPI-unaware designer tabs. Previously, the Windows
Designer tab in Visual Studio ran at the DPI of Visual Studio. This causes problems when
you're designing a DPI-unaware Windows Forms app. Now you can ensure that the
designer runs at the same scale as you want the app to run, either DPI-aware or not.
Before this feature was introduced, you had to run Visual Studio in DPI-unaware mode,
which made Visual Studio itself blurry when scaling was applied in Windows. Now you
can leave Visual Studio alone and let the designer run DPI-unaware.

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.

High DPI improvements


High DPI rendering with PerMonitorV2 has been improved:

Correctly scale nested controls. For example, a button that's in a panel, which is
placed on a tab page.

Scale Form.MaximumSize and Form.MinimumSize properties based on the current


monitor DPI settings.

Starting with .NET 8, this feature is enabled by default and you need to opt out of
it to revert to the previous behavior.

To disable the feature, add


System.Windows.Forms.ScaleTopLevelFormMinMaxSizeForDpi to the configProperties

setting in runtimeconfig.json, and set the value to false:

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

Due to working with underlying external dependencies, such as Xcode or Android


SDK Tools, the .NET Multi-platform App UI (.NET MAUI) support policy differs from
the .NET and .NET Core support policy . For more information, see .NET MAUI
support policy .

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.

Type deprecation and removal


The following types or members have been deprecated:

ClickGestureRecognizer has been deprecated.


The AutomationProperties.Name, AutomationProperties.HelpText, and
AutomationProperties.LabeledBy attached properties have been deprecated.
The FocusChangeRequested has been deprecated. Instead, use the Focus() method
to attempt to set focus on the view.

The following types or members have been removed:

The Application.Properties property and Application.SavePropertiesAsync


method have been removed. To migrate your app properties data to .NET MAUI,
see Migrate data from the Xamarin.Forms app properties dictionary to .NET MAUI
preferences.
The PhoneDialer.Current property has been removed. Use PhoneDialer.Default
instead.
OpenGLView has been removed.

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

namespace, can be called to disable this default behavior. The


KeyboardAutoManagerScroll.Connect method can be called to re-enable the

behavior after it's been disabled.


How the color of a tab is set in a Shell app has changed on some platforms. For
more information, see Tab appearance.
It's not required to specify a value for the $(ApplicationIdGuid) build property in
your app's project file. This is because .NET MAUI Windows apps no longer require
a GUID as an app ID, and instead use the value of the $(ApplicationId) build
property as the app ID. Therefore, the same reverse domain format app ID is now
used across all platforms, such as com.mycompany.myapp.
.NET MAUI Mac Catalyst apps are no longer limited to 50 menu items on the menu
bar.
The PlatformImage.FromStream method, in the Microsoft.Maui.Graphics
namespace, can now be used to load images on Windows instead of having to use
the W2DImageLoadingService class.
On Android, animations respect the system animation settings. For more
information, see Basic animation.

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

For more information, see .NET 8 Performance Improvements in .NET MAUI .

Upgrade from .NET 7 to .NET 8


To upgrade your projects from .NET 7 to .NET 8, install .NET 8 and the .NET MAUI
workload with Visual Studio 17.8+ , or with the standalone installer and the dotnet
workload install maui command.

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>

The following example shows the TFMs for a .NET 8 project:

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

EF Core 8.0 (EF8) was released in November 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.

Value objects using Complex Types


Objects saved to the database can be split into three broad categories:

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:

Are not identified or tracked by key value.


Must be defined as part of an entity type. (In other words, you cannot have a
DbSet of a complex type.)

Can be either .NET value types or reference types.


Instances can be shared by multiple properties.

Simple example
For example, consider an Address type:

C#

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; }
}

Address is then used in three places in a simple customer/orders model:

C#

public class Customer


{
public int Id { get; set; }
public required string Name { get; set; }
public required Address Address { get; set; }
public List<Order> Orders { get; } = new();
}

public class Order


{
public int Id { get; set; }
public required string Contents { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}

Let's create and save a customer with their address:

C#

var customer = new Customer


{
Name = "Willow",
Address = new() { Line1 = "Barking Gate", City = "Walpole St Peter",
Country = "UK", PostCode = "PE14 7AV" }
};

context.Add(customer);
await context.SaveChangesAsync();

This results in the following row being inserted into the database:
SQL

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country],


[Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

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

INSERT INTO [Orders] ([Contents], [CustomerId],


[BillingAddress_City], [BillingAddress_Country], [BillingAddress_Line1],
[BillingAddress_Line2], [BillingAddress_PostCode],
[ShippingAddress_City], [ShippingAddress_Country],
[ShippingAddress_Line1], [ShippingAddress_Line2],
[ShippingAddress_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9, @p10, @p11);

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

warn: 8/20/2023 12:48:01.678


CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001]
(Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types
'Order.BillingAddress#Address' and 'Customer.Address#Address' with defining
navigations. If a property value changes, it will result in two store
changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687
CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001]
(Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types
'Order.ShippingAddress#Address' and 'Customer.Address#Address' with defining
navigations. If a property value changes, it will result in two store
changes, which might not be the desired outcome.
warn: 8/20/2023 12:48:01.687
CoreEventId.DuplicateDependentEntityTypeInstanceWarning[10001]
(Microsoft.EntityFrameworkCore.Update)
The same entity is being tracked as different entity types
'Order.ShippingAddress#Address' and 'Order.BillingAddress#Address' with
defining navigations. If a property value changes, it will result in two
store changes, which might not be the desired outcome.
fail: 8/20/2023 12:48:01.709 CoreEventId.SaveChangesFailed[10000]
(Microsoft.EntityFrameworkCore.Update)
An exception occurred in the database while saving changes for context
type 'NewInEfCore8.ComplexTypesSample+CustomerContext'.
System.InvalidOperationException: Cannot save instance of
'Order.ShippingAddress#Address' because it is an owned entity without any
reference to its owner. Owned entities can only be saved as part of an
aggregate also including the owner entity.
at
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.Pr
epareToSave()

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.

Configuration of complex types


Complex types must be configured in the model using either mapping attributes or by
calling ComplexProperty API in OnModelCreating. Complex types are not discovered by
convention.

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#

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Customer>()
.ComplexProperty(e => e.Address);

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#

customer.Address.Line1 = "Peacock Lodge";


await context.SaveChangesAsync();

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

If order addresses should change automatically when the customer address


changes, then consider mapping the address as an entity type. Order and Customer
can then safely reference the same address instance (which is now identified by a
key) via a navigation property.

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.

Reference types as complex types

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#

public class Address


{
public Address(string line1, string? line2, string city, string country,
string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}

public string Line1 { get; }


public string? Line2 { get; }
public string City { get; }
public string Country { get; }
public string PostCode { get; }
}

 Tip

With C# 12 or above, this class definition can be simplified using a primary


constructor:

C#

public class Address(string line1, string? line2, string city, string


country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}

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#

var currentAddress = customer.Address;


customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City,
currentAddress.Country, currentAddress.PostCode);

await context.SaveChangesAsync();

This time the call to SaveChangesAsync only updates the customer address:

SQL

UPDATE [Customers] SET [Address_Line1] = @p0


OUTPUT 1
WHERE [Id] = @p1;

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#

public record Address


{
public Address(string line1, string? line2, string city, string country,
string postCode)
{
Line1 = line1;
Line2 = line2;
City = city;
Country = country;
PostCode = postCode;
}

public string Line1 { get; init; }


public string? Line2 { get; init; }
public string City { get; init; }
public string Country { get; init; }
public string PostCode { get; init; }
}

 Tip

This record definition can be simplified using a primary constructor:

C#

public record Address(string Line1, string? Line2, string City, string


Country, string PostCode);

Replacing the mutable object and calling SaveChanges now requires less code:

C#
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

Value types as complex types

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#

public struct 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; }
}

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#

public readonly struct Address(string line1, string? line2, string city,


string country, string postCode)
{
public string Line1 { get; } = line1;
public string? Line2 { get; } = line2;
public string City { get; } = city;
public string Country { get; } = country;
public string PostCode { get; } = postCode;
}

The code for changing the address now looks the same as when using immutable class:

C#

var currentAddress = customer.Address;


customer.Address = new Address(
"Peacock Lodge", currentAddress.Line2, currentAddress.City,
currentAddress.Country, currentAddress.PostCode);

await context.SaveChangesAsync();

Immutable struct record

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#

public readonly record struct Address(string Line1, string? Line2, string


City, string Country, string PostCode);

The code for changing the address now looks the same as when using immutable class
record:

C#

customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

Nested complex types


A complex type can contain properties of other complex types. For example, let's use
our Address complex type from above together with a PhoneNumber complex type, and
nest them both inside another complex type:

C#
public record Address(string Line1, string? Line2, string City, string
Country, string PostCode);

public record PhoneNumber(int CountryCode, long Number);

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; }
}

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.

We will add Contact as a property of the Customer :

C#

public class Customer


{
public int Id { get; set; }
public required string Name { get; set; }
public required Contact Contact { get; set; }
public List<Order> Orders { get; } = new();
}

And PhoneNumber as properties of the Order :

C#

public class Order


{
public int Id { get; set; }
public required string Contents { get; set; }
public required PhoneNumber ContactPhone { get; set; }
public required Address ShippingAddress { get; set; }
public required Address BillingAddress { get; set; }
public Customer Customer { get; set; } = null!;
}

Configuration of nested complex types can again be achieved using


ComplexTypeAttribute:

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#

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<Customer>(
b =>
{
b.ComplexProperty(
e => e.Contact,
b =>
{
b.ComplexProperty(e => e.Address);
b.ComplexProperty(e => e.HomePhone);
b.ComplexProperty(e => e.WorkPhone);
b.ComplexProperty(e => e.MobilePhone);
});
});

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#

var customer = await context.Customers.FirstAsync(e => e.Id == customerId);

Is translated to the following SQL when using SQL Server:

SQL

SELECT TOP(1) [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].


[Contact_Address_Country],
[c].[Contact_Address_Line1], [c].[Contact_Address_Line2], [c].
[Contact_Address_PostCode],
[c].[Contact_HomePhone_CountryCode], [c].[Contact_HomePhone_Number],
[c].[Contact_MobilePhone_CountryCode],
[c].[Contact_MobilePhone_Number], [c].[Contact_WorkPhone_CountryCode],
[c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__customerId_0

Notice two things from this 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();

This translates to the following when using SQL Server:

SQL

SELECT TOP(2) [o].[ShippingAddress_City], [o].[ShippingAddress_Country],


[o].[ShippingAddress_Line1],
[o].[ShippingAddress_Line2], [o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[Id] = @__orderId_0

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#

var city = "Walpole St Peter";


var walpoleOrders = await context.Orders.Where(e => e.ShippingAddress.City
== city).ToListAsync();

Which translates to the following SQL on SQL Server:

SQL

SELECT [o].[Id], [o].[Contents], [o].[CustomerId], [o].


[BillingAddress_City], [o].[BillingAddress_Country],
[o].[BillingAddress_Line1], [o].[BillingAddress_Line2], [o].
[BillingAddress_PostCode],
[o].[ContactPhone_CountryCode], [o].[ContactPhone_Number], [o].
[ShippingAddress_City],
[o].[ShippingAddress_Country], [o].[ShippingAddress_Line1], [o].
[ShippingAddress_Line2],
[o].[ShippingAddress_PostCode]
FROM [Orders] AS [o]
WHERE [o].[ShippingAddress_City] = @__city_0
A full complex type instance can also be used in predicates. For example, finding all
customers with a given phone number:

C#

var phoneNumber = new PhoneNumber(44, 7777555777);


var customersWithNumber = await context.Customers
.Where(
e => e.Contact.MobilePhone == phoneNumber
|| e.Contact.WorkPhone == phoneNumber
|| e.Contact.HomePhone == phoneNumber)
.ToListAsync();

This translates to the following SQL when using SQL Server:

SQL

SELECT [c].[Id], [c].[Name], [c].[Contact_Address_City], [c].


[Contact_Address_Country], [c].[Contact_Address_Line1],
[c].[Contact_Address_Line2], [c].[Contact_Address_PostCode], [c].
[Contact_HomePhone_CountryCode],
[c].[Contact_HomePhone_Number], [c].[Contact_MobilePhone_CountryCode],
[c].[Contact_MobilePhone_Number],
[c].[Contact_WorkPhone_CountryCode], [c].[Contact_WorkPhone_Number]
FROM [Customers] AS [c]
WHERE ([c].[Contact_MobilePhone_CountryCode] =
@__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_MobilePhone_Number] =
@__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_WorkPhone_CountryCode] =
@__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_WorkPhone_Number] =
@__entity_equality_phoneNumber_0_Number)
OR ([c].[Contact_HomePhone_CountryCode] =
@__entity_equality_phoneNumber_0_CountryCode
AND [c].[Contact_HomePhone_Number] =
@__entity_equality_phoneNumber_0_Number)

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.

Manipulation of complex type values


EF8 provides access to tracking information such as the current and original values of
complex types and whether or not a property value has been modified. The API complex
types is an extension of the change tracking API already used for entity types.
The ComplexProperty methods of EntityEntry return a entry for an entire complex object.
For example, to get the current value of the Order.BillingAddress :

C#

var billingAddress = context.Entry(order)


.ComplexProperty(e => e.BillingAddress)
.CurrentValue;

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#

var postCode = context.Entry(order)


.ComplexProperty(e => e.BillingAddress)
.Property(e => e.PostCode)
.CurrentValue;

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#

var currentCity = context.Entry(customer)


.ComplexProperty(e => e.Contact)
.ComplexProperty(e => e.Address)
.Property(e => e.City)
.CurrentValue;

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.

Complex type limitations in EF8 include:

Support collections of complex types. (Issue #31237 )


Allow complex type properties to be null. (Issue #31376 )
Map complex type properties to JSON columns. (Issue #31252 )
Constructor injection for complex types. (Issue #31621 )
Add seed data support for complex types. (Issue #31254 )
Map complex type properties for the Cosmos provider. (Issue #31253 )
Implement complex types for the in-memory database. (Issue #31464 )

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

The code shown here comes from PrimitiveCollectionsSample.cs .


Primitive collection properties
EF Core can map any IEnumerable<T> property, where T is a primitive type, to a JSON
column in the database. This is done by convention for public properties which have
both a getter and a setter. For example, all properties in the following entity type are
mapped to JSON columns by convention:

C#

public class PrimitiveCollections


{
public IEnumerable<int> Ints { get; set; }
public ICollection<string> Strings { get; set; }
public IList<DateOnly> Dates { get; set; }
public uint[] UnsignedInts { get; set; }
public List<bool> Booleans { get; set; }
public List<Uri> Urls { get; set; }
}

7 Note

What do we mean by "primitive type" in this context? Essentially, something that


the database provider knows how to map, using some kind of value conversion if
necessary. For example, in the entity type above, the types int , string , DateTime ,
DateOnly and bool are all handled without conversion by the database provider.

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

for these types.

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);

Or, using mapping attributes:


C#

[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#

protected override void ConfigureConventions(ModelConfigurationBuilder


configurationBuilder)
{
configurationBuilder
.Properties<List<DateOnly>>()
.AreUnicode(false)
.HaveMaxLength(4000);
}

Queries with primitive collections


Let's look at some of the queries that make use of collections of primitive types. For this,
we'll need a simple model with two entity types. The first represents a British public
house , or "pub":

C#

public class Pub


{
public Pub(string name, string[] beers)
{
Name = name;
Beers = beers;
}

public int Id { get; set; }


public string Name { get; set; }
public string[] Beers { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
}

The Pub type contains two primitive collections:

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#

public class DogWalk


{
public DogWalk(string name)
{
Name = name;
}

public int Id { get; set; }


public string Name { get; set; }
public Terrain Terrain { get; set; }
public List<DateOnly> DaysVisited { get; private set; } = new();
public Pub ClosestPub { get; set; } = null!;
}

public enum Terrain


{
Forest,
River,
Hills,
Village,
Park,
Beach,
}

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#

var terrains = new[] { Terrain.River, Terrain.Beach, Terrain.Park };


var walksWithTerrain = await context.Walks
.Where(e => terrains.Contains(e.Terrain))
.Select(e => e.Name)
.ToListAsync();

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]'

The query then uses OpenJson on SQL Server:

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#

protected override void OnConfiguring(DbContextOptionsBuilder


optionsBuilder)
=> optionsBuilder
.UseSqlServer(
@"Data Source=
(LocalDb)\MSSQLLocalDB;Database=AllTogetherNow",
sqlServerOptionsBuilder =>
sqlServerOptionsBuilder.UseCompatibilityLevel(120));

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#

var beer = "Heineken";


var pubsWithHeineken = await context.Pubs
.Where(e => e.Beers.Contains(beer))
.Select(e => e.Name)
.ToListAsync();

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

matched to the passed parameter.

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#

var beers = new[] { "Carling", "Heineken", "Stella Artois", "Carlsberg" };


var pubsWithLager = await context.Pubs
.Where(e => beers.Any(b => e.Beers.Contains(b)))
.Select(e => e.Name)
.ToListAsync();

This translates to the following on SQL Server:

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

The @__beers_0 parameter value here is ["Carling","Heineken","Stella


Artois","Carlsberg"] .

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#

var thisYear = DateTime.Now.Year;


var pubsVisitedThisYear = await context.Pubs
.Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
.Select(e => e.Name)
.ToListAsync();

This translates to the following on SQL Server:


SQL

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#

var pubsVisitedInOrder = await context.Pubs


.Select(e => new
{
e.Name,
FirstVisited = e.DaysVisited.OrderBy(v => v).First(),
LastVisited = e.DaysVisited.OrderByDescending(v => v).First(),
})
.OrderBy(p => p.FirstVisited)
.ToListAsync();

This translates to the following on SQL Server:

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#

var walksWithADrink = await context.Walks.Select(


w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v =>
w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();

This translates to the following on SQL Server:

SQL

SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], (


SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson([p].[DaysVisited]) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR
([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson([w].[DaysVisited]) AS [d1]) AS [TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]

And reveals the following data:

none

The Prince of Wales Feathers was visited 5 times in 8 "Ailsworth to Nene"


walks.
The Prince of Wales Feathers was visited 6 times in 9 "Caster Hanglands"
walks.
The Royal Oak was visited 6 times in 8 "Ferry Meadows" walks.
The White Swan was visited 7 times in 9 "Woodnewton" walks.
The Eltisley was visited 6 times in 8 "Eltisley" walks.
Farr Bay Inn was visited 7 times in 11 "Farr Beach" walks.
Farr Bay Inn was visited 7 times in 9 "Newlands" walks.

Looks like beer and dog walking are a winning combination!


Primitive collections in JSON documents
In all the examples above, column for primitive collection contains JSON. However, this
is not the same as mapping an owned entity type to a column containing a JSON
document, which was introduced in EF7. But what if that JSON document itself contains
a primitive collection? Well, all the queries above still work in the same way! For
example, imagine we move the days visited data into an owned type Visits mapped to
a JSON document:

C#

public class Pub


{
public Pub(string name)
{
Name = name;
}

public int Id { get; set; }


public string Name { get; set; }
public BeerData Beers { get; set; } = null!;
public Visits Visits { get; set; } = null!;
}

public class Visits


{
public string? LocationTag { get; set; }
public List<DateOnly> DaysVisited { get; set; } = null!;
}

 Tip

The code shown here comes from PrimitiveCollectionsInJsonSample.cs .

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#

var walksWithADrink = await context.Walks.Select(


w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
WalkLocationTag = w.Visits.LocationTag,
PubLocationTag = w.ClosestPub.Visits.LocationTag,
Count = w.Visits.DaysVisited.Count(v =>
w.ClosestPub.Visits.DaysVisited.Contains(v)),
TotalCount = w.Visits.DaysVisited.Count
}).ToListAsync();

This translates to the following on SQL Server:

SQL

SELECT [w].[Name] AS [WalkName], [p].[Name] AS [PubName], JSON_VALUE([w].


[Visits], '$.LocationTag') AS [WalkLocationTag], JSON_VALUE([p].[Visits],
'$.LocationTag') AS [PubLocationTag], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d]
WHERE EXISTS (
SELECT 1
FROM OpenJson(JSON_VALUE([p].[Visits], '$.DaysVisited')) AS [d0]
WHERE CAST([d0].[value] AS date) = CAST([d].[value] AS date) OR
([d0].[value] IS NULL AND [d].[value] IS NULL))) AS [Count], (
SELECT COUNT(*)
FROM OpenJson(JSON_VALUE([w].[Visits], '$.DaysVisited')) AS [d1]) AS
[TotalCount]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]

And to a similar query when using SQLite:

SQL

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", "w"."Visits" ->>


'LocationTag' AS "WalkLocationTag", "p"."Visits" ->> 'LocationTag' AS
"PubLocationTag", (
SELECT COUNT(*)
FROM json_each("w"."Visits" ->> 'DaysVisited') AS "d"
WHERE EXISTS (
SELECT 1
FROM json_each("p"."Visits" ->> 'DaysVisited') AS "d0"
WHERE "d0"."value" = "d"."value")) AS "Count",
json_array_length("w"."Visits" ->> 'DaysVisited') AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

 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;
}

public string Name { get; private set; }


}

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#

public class Pub


{
public Pub(string name)
{
Name = name;
}

public int Id { get; set; }


public string Name { get; set; }
public List<Beer> Beers { get; set; } = new();
public List<DateOnly> DaysVisited { get; private set; } = new();
}

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

CREATE TABLE [Beer] (


[PubId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Beer] PRIMARY KEY ([PubId], [Id]),
CONSTRAINT [FK_Beer_Pubs_PubId] FOREIGN KEY ([PubId]) REFERENCES [Pubs]
([Id]) ON DELETE CASCADE

Enhancements to JSON column mapping


EF8 includes improvements to the JSON column mapping support introduced in EF7.

 Tip

The code shown here comes from JsonColumnsSample.cs .

Translate element access into JSON arrays


EF8 supports indexing in JSON arrays when executing queries. For example, the
following query checks whether the first two updates were made before a given date.

C#

var cutoff = DateOnly.FromDateTime(DateTime.UtcNow -


TimeSpan.FromDays(365));
var updatedPosts = await context.Posts
.Where(
p => p.Metadata!.Updates[0].UpdatedOn < cutoff
&& p.Metadata!.Updates[1].UpdatedOn < cutoff)
.ToListAsync();

This translates into the following SQL when using SQL Server:

SQL

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].


[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE CAST(JSON_VALUE([p].[Metadata],'$.Updates[0].UpdatedOn') AS date) <
@__cutoff_0
AND CAST(JSON_VALUE([p].[Metadata],'$.Updates[1].UpdatedOn') AS date) <
@__cutoff_0

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#

var postsAndRecentUpdatesNullable = await context.Posts


.Select(p => new
{
p.Title,
LatestUpdate = (DateOnly?)p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = (DateOnly?)p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();

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#

var postsAndRecentUpdates = await context.Posts


.Where(p => p.Metadata!.Updates[0].UpdatedOn != null
&& p.Metadata!.Updates[1].UpdatedOn != null)
.Select(p => new
{
p.Title,
LatestUpdate = p.Metadata!.Updates[0].UpdatedOn,
SecondLatestUpdate = p.Metadata.Updates[1].UpdatedOn
})
.ToListAsync();

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)

Translate queries into embedded collections


EF8 supports queries against collections of both primitive (discussed above) and non-
primitive types embedded in the JSON document. For example, the following query
returns all posts with any of an arbitrary list of search terms:

C#

var searchTerms = new[] { "Search #2", "Search #3", "Search #5", "Search
#8", "Search #13", "Search #21", "Search #34" };

var postsWithSearchTerms = await context.Posts


.Where(post => post.Metadata!.TopSearches.Any(s =>
searchTerms.Contains(s.Term)))
.ToListAsync();

This translates into the following SQL when using SQL Server:

SQL

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].


[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].
[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE EXISTS (
SELECT 1
FROM OPENJSON([p].[Metadata], '$.TopSearches') WITH (
[Count] int '$.Count',
[Term] nvarchar(max) '$.Term'
) AS [t]
WHERE EXISTS (
SELECT 1
FROM OPENJSON(@__searchTerms_0) WITH ([value] nvarchar(max) '$') AS
[s]
WHERE [s].[value] = [t].[Term]))
JSON Columns for SQLite
EF7 introduced support for mapping to JSON columns when using Azure SQL/SQL
Server. EF8 extends this support to SQLite databases. As for the SQL Server support, this
includes:

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

Queries into JSON columns on SQLite use the json_extract function. For example, the
"authors in Chigley" query from the documentation referenced above:

C#

var authorsInChigley = await context.Authors


.Where(author => author.Contact.Address.City == "Chigley")
.ToListAsync();

Is translated to the following SQL when using SQLite:

SQL

SELECT "a"."Id", "a"."Name", "a"."Contact"


FROM "Authors" AS "a"
WHERE json_extract("a"."Contact", '$.Address.City') = 'Chigley'

Updating JSON columns


For updates, EF uses the json_set function on SQLite. For example, when updating a
single property in a document:

C#

var arthur = await context.Authors.SingleAsync(author =>


author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();

EF generates the following parameters:

text

info: 3/10/2023 10:51:33.127 RelationalEventId.CommandExecuted[20101]


(Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='["United Kingdom"]'
(Nullable = false) (Size = 18), @p1='4'], CommandType='Text',
CommandTimeout='30']

Which use the json_set function on SQLite:

SQL

UPDATE "Authors" SET "Contact" = json_set("Contact", '$.Address.Country',


json_extract(@p0, '$[0]'))
WHERE "Id" = @p1
RETURNING 1;

HierarchyId in .NET and EF Core


Azure SQL and SQL Server have a special data type called hierarchyid that is used to
store hierarchical data. In this case, "hierarchical data" essentially means data that forms
a tree structure, where each item can have a parent and/or children. Examples of such
data are:

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.

Support in .NET and EF Core


Official support for the SQL Server hierarchyid type has only recently come to modern
.NET platforms (i.e. ".NET Core"). This support is in the form of the
Microsoft.SqlServer.Types NuGet package, which brings in low-level SQL Server-
specific types. In this case, the low-level type is called SqlHierarchyId .

At the next level, a new Microsoft.EntityFrameworkCore.SqlServer.Abstractions


package has been introduced, which includes a higher-level HierarchyId type intended
for use in entity types.

 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#

public class Halfling


{
public Halfling(HierarchyId pathFromPatriarch, string name, int?
yearOfBirth = null)
{
PathFromPatriarch = pathFromPatriarch;
Name = name;
YearOfBirth = yearOfBirth;
}

public int Id { get; private set; }


public HierarchyId PathFromPatriarch { get; set; }
public string Name { get; set; }
public int? YearOfBirth { get; set; }
}

 Tip

The code shown here and in the examples below comes from
HierarchyIdSample.cs .

 Tip

If desired, HierarchyId is suitable for use as a key property type.


In this case, the family tree is rooted with the patriarch of the family. Each halfling can
be traced from the patriarch down the tree using its PathFromPatriarch property. SQL
Server uses a compact binary format for these paths, but it is common to parse to and
from a human-readable string representation when when working with code. In this
representation, the position at each level is separated by a / character. For example,
consider the family tree in the diagram below:

In this tree:

Balbo is at the root of the tree, represented by / .


Balbo has five children, represented by /1/ , /2/ , /3/ , /4/ , and /5/ .
Balbo's first child, Mungo, also has five children, represented by /1/1/ , /1/2/ ,
/1/3/ , /1/4/ , and /1/5/ . Notice that the HierarchyId for Balbo ( /1/ ) is the prefix

for all his children.


Similarly, Balbo's third child, Ponto, has two children, represented by /3/1/ and
/3/2/ . Again the each of these children is prefixed by the HierarchyId for Ponto,

which is represented as /3/ .


And so on down the 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

GetAncestor(int n) Gets the node n levels up the hierarchical tree.

GetDescendant(HierarchyId? child1, Gets the value of a descendant node that is greater


HierarchyId? child2) than child1 and less than child2 .

GetLevel() Gets the level of this node in the hierarchical tree.


Method Description

GetReparentedValue(HierarchyId? Gets a value representing the location of a new node


oldRoot, HierarchyId? newRoot) that has a path from newRoot equal to the path from
oldRoot to this, effectively moving this to the new
location.

IsDescendantOf(HierarchyId? parent) Gets a value indicating whether this node is a


descendant of parent .

In addition, the operators == , != , < , <= , > and >= can be used.

The following are examples of using these methods in LINQ queries.

Get entities at a given level in the tree

The following query uses GetLevel to return all halflings at a given level in the family
tree:

C#

var generation = await context.Halflings.Where(halfling =>


halfling.PathFromPatriarch.GetLevel() == level).ToListAsync();

This translates to the following SQL:

SQL

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]


FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetLevel() = @__level_0

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

Get the direct ancestor of an entity

The following query uses GetAncestor to find the direct ancestor of a halfling, given that
halfling's name:
C#

async Task<Halfling?> FindDirectAncestor(string name)


=> await context.Halflings
.SingleOrDefaultAsync(
ancestor => ancestor.PathFromPatriarch == context.Halflings
.Single(descendent => descendent.Name ==
name).PathFromPatriarch
.GetAncestor(1));

This translates to the following SQL:

SQL

SELECT TOP(2) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].


[YearOfBirth]
FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch] = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0).GetAncestor(1)

Running this query for the halfling "Bilbo" returns "Bungo".

Get the direct descendents of an entity

The following query also uses GetAncestor , but this time to find the direct descendents
of a halfling, given that halfling's name:

C#

IQueryable<Halfling> FindDirectDescendents(string name)


=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.GetAncestor(1) ==
context.Halflings
.Single(ancestor => ancestor.Name == name).PathFromPatriarch);

This translates to the following SQL:

SQL

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]


FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].GetAncestor(1) = (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0)
Running this query for the halfling "Mungo" returns "Bungo", "Belba", "Longo", and
"Linda".

Get all ancestors of an entity

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#

IQueryable<Halfling> FindAllAncestors(string name)


=> context.Halflings.Where(
ancestor => context.Halflings
.Single(
descendent =>
descendent.Name == name
&& ancestor.Id != descendent.Id)

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

This translates to the following SQL:

SQL

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]


FROM [Halflings] AS [h]
WHERE (
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].
[Id]).IsDescendantOf([h].[PathFromPatriarch]) = CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

Running this query for the halfling "Bilbo" returns "Bungo", "Mungo", and "Balbo".

Get all descendents of an entity


The following query also uses IsDescendantOf , but this time to all the descendents of a
halfling, given that halfling's name:

C#

IQueryable<Halfling> FindAllDescendents(string name)


=> context.Halflings.Where(
descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings
.Single(
ancestor =>
ancestor.Name == name
&& descendent.Id != ancestor.Id)
.PathFromPatriarch))
.OrderBy(descendent => descendent.PathFromPatriarch.GetLevel());

This translates to the following SQL:

SQL

SELECT [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].[YearOfBirth]


FROM [Halflings] AS [h]
WHERE [h].[PathFromPatriarch].IsDescendantOf((
SELECT TOP(1) [h0].[PathFromPatriarch]
FROM [Halflings] AS [h0]
WHERE [h0].[Name] = @__name_0 AND [h].[Id] <> [h0].[Id])) = CAST(1 AS
bit)
ORDER BY [h].[PathFromPatriarch].GetLevel()

Running this query for the halfling "Mungo" returns "Bungo", "Belba", "Longo", "Linda",
"Bingo", "Bilbo", "Otho", "Falco", "Lotho", and "Poppy".

Finding a common ancestor

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#

async Task<Halfling?> FindCommonAncestor(Halfling first, Halfling second)


=> await context.Halflings
.Where(
ancestor =>
first.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch)
&&
second.PathFromPatriarch.IsDescendantOf(ancestor.PathFromPatriarch))
.OrderByDescending(ancestor =>
ancestor.PathFromPatriarch.GetLevel())
.FirstOrDefaultAsync();

This translates to the following SQL:

SQL

SELECT TOP(1) [h].[Id], [h].[Name], [h].[PathFromPatriarch], [h].


[YearOfBirth]
FROM [Halflings] AS [h]
WHERE @__first_PathFromPatriarch_0.IsDescendantOf([h].[PathFromPatriarch]) =
CAST(1 AS bit)
AND @__second_PathFromPatriarch_1.IsDescendantOf([h].[PathFromPatriarch])
= CAST(1 AS bit)
ORDER BY [h].[PathFromPatriarch].GetLevel() DESC

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#

var longoAndDescendents = await context.Halflings.Where(


descendent => descendent.PathFromPatriarch.IsDescendantOf(
context.Halflings.Single(ancestor => ancestor.Name ==
"Longo").PathFromPatriarch))
.ToListAsync();

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();

This results in the following database update:

SQL

SET NOCOUNT ON;


UPDATE [Halflings] SET [PathFromPatriarch] = @p0
OUTPUT 1
WHERE [Id] = @p1;
UPDATE [Halflings] SET [PathFromPatriarch] = @p2
OUTPUT 1
WHERE [Id] = @p3;
UPDATE [Halflings] SET [PathFromPatriarch] = @p4
OUTPUT 1
WHERE [Id] = @p5;

Using these parameters:

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

The code shown here comes from RawSqlSample.cs .

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

CREATE TABLE [Posts] (


[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Content] nvarchar(max) NOT NULL,
[PublishedOn] date NOT NULL,
[BlogId] int NOT NULL,
);

SqlQuery can be used to query this table and return instances of a BlogPost type with

properties corresponding to the columns in the table:

For example:

C#

public class BlogPost


{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateOnly PublishedOn { get; set; }
public int BlogId { get; set; }
}

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();

This query is parameterized and executed as:

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#

public class BlogPost


{
public BlogPost(string blogTitle, string content, DateOnly publishedOn)
{
BlogTitle = blogTitle;
Content = content;
PublishedOn = publishedOn;
}

public int Id { get; private set; }

[Column("Title")]
public string BlogTitle { get; set; }

public string Content { get; set; }


public DateOnly PublishedOn { get; set; }
public int BlogId { 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#

public class PostSummary


{
public string BlogName { get; set; } = null!;
public string PostTitle { get; set; } = null!;
public DateOnly? PublishedOn { get; set; }
}

And can be queried using SqlQuery in the same way as before:

C#

var cutoffDate = new DateOnly(2022, 1, 1);


var summaries =
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.PublishedOn >= {cutoffDate}")
.ToListAsync();

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();

This is executed as:

SQL

SELECT [n].[BlogName], [n].[PostTitle], [n].[PublishedOn]


FROM (
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
) AS [n]
WHERE [n].[PublishedOn] >= @__cutoffDate_1 AND [n].[PublishedOn] < @__end_2

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();

Which translates to much cleaner SQL:

SQL

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].[PublishedOn] >= @__start_0 AND [p].[PublishedOn] < @__end_1

 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();

Likewise, SqlQuery can be used for the results of a function:

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

Lazy-loading for no-tracking queries


EF8 adds support for lazy-loading of navigations on entities that are not being tracked
by the DbContext . This means a no-tracking query can be followed by lazy-loading of
navigations on the entities returned by the no-tracking query.

 Tip

The code for the lazy-loading examples shown below comes from
LazyLoadingSample.cs .

For example, consider a no-tracking query for blogs:


C#

var blogs = await context.Blogs.AsNoTracking().ToListAsync();

If Blog.Posts is configured for lazy-loading, for example, using lazy-loading proxies,


then accessing Posts will cause it to load from the database:

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#

foreach (var blog in blogs)


{
if (context.Entry(blog).Collection(e => e.Posts).IsLoaded)
{
Console.WriteLine($" Posts for blog '{blog.Name}' are loaded.");
}
}

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.

Explicit loading from untracked entities


EF8 supports loading of navigations on untracked entities even when the entity or
navigation is not configured for lazy-loading. Unlike with lazy-loading, this explicit
loading can be done asynchronously. For example:

C#

await context.Entry(blog).Collection(e => e.Posts).LoadAsync();

Opt-out of lazy-loading for specific navigations


EF8 allows configuration of specific navigations to not lazy-load, even when everything
else is set up to do so. For example, to configure the Post.Author navigation to not
lazy-load, do the following:

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.

Lazy-loading proxies work by overriding virtual navigation properties. In classic EF6


applications, a common source of bugs is forgetting to make a navigation virtual, since
the navigation will then silently not lazy-load. Therefore, EF Core proxies throw by
default when a navigation is not virtual.

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#

optionsBuilder.UseLazyLoadingProxies(b => b.IgnoreNonVirtualNavigations());


Access to tracked entities

Lookup tracked entities by primary, alternate, or foreign


key
Internally, EF maintains data structures for finding tracked entities by primary, alternate,
or foreign key. These data structures are used for efficient fixup between related entities
when new entities are tracked or relationships change.

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#

var blogEntry = context.Blogs.Local.FindEntry(2)!;

 Tip

The code shown here comes from LookupByKeySample.cs .

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#

Console.WriteLine($"Blog '{blogEntry.Entity.Name}' with key


{blogEntry.Entity.Id} is tracked in the '{blogEntry.State}' state.");

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#

var siteEntry = context.Websites.Local.FindEntry(nameof(Website.Uri), new


Uri("https://www.bricelam.net/"))!;

Or to look up by a unique foreign key:


C#

var blogAtSiteEntry = context.Blogs.Local.FindEntry(nameof(Blog.SiteUri),


new Uri("https://www.bricelam.net/"))!;

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#

var postEntries = context.Posts.Local.GetEntries(nameof(Post.BlogId), 2);

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

Finally, it is also possible to perform lookups against composite keys, other


combinations of multiple properties, or when the property type is not known at compile
time. For example:

C#

var postTagEntry = context.Set<PostTag>().Local.FindEntryUntyped(new


object[] { 4, "TagEF" });

Model building

Discriminator columns have max length


In EF8, string discriminator columns used for TPH inheritance mapping are now
configured with a max length. This length is calculated as the smallest Fibonacci number
that covers all defined discriminator values. For example, consider the following
hierarchy:

C#

public abstract class Document


{
public int Id { get; set; }
public string Title { get; set; }
}

public abstract class Book : Document


{
public string? Isbn { get; set; }
}

public class PaperbackEdition : Book


{
}

public class HardbackEdition : Book


{
}

public class Magazine : Document


{
public int IssueNumber { get; set; }
}

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

CREATE TABLE [Documents] (


[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Discriminator] nvarchar(21) NOT NULL,
[Isbn] nvarchar(max) NULL,
[IssueNumber] int NULL,
CONSTRAINT [PK_Documents] PRIMARY KEY ([Id]),

 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

DateOnly and TimeOnly can be used in EF Core 6 and 7 using the

ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly community package


from @ErikEJ .

For example, consider the following EF model for British schools:

C#

public class School


{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly Founded { get; set; }
public List<Term> Terms { get; } = new();
public List<OpeningHours> OpeningHours { get; } = new();
}

public class Term


{
public int Id { get; set; }
public string Name { get; set; } = null!;
public DateOnly FirstDay { get; set; }
public DateOnly LastDay { get; set; }
public School School { get; set; } = null!;
}

[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

The code shown here comes from DateOnlyTimeOnlySample.cs .

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

CREATE TABLE [Schools] (


[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Founded] date NOT NULL,
CONSTRAINT [PK_Schools] PRIMARY KEY ([Id]));

CREATE TABLE [OpeningHours] (


[SchoolId] int NOT NULL,
[Id] int NOT NULL IDENTITY,
[DayOfWeek] int NOT NULL,
[OpensAt] time NULL,
[ClosesAt] time NULL,
CONSTRAINT [PK_OpeningHours] PRIMARY KEY ([SchoolId], [Id]),
CONSTRAINT [FK_OpeningHours_Schools_SchoolId] FOREIGN KEY ([SchoolId])
REFERENCES [Schools] ([Id]) ON DELETE CASCADE);

CREATE TABLE [Term] (


[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[FirstDay] date NOT NULL,
[LastDay] date NOT NULL,
[SchoolId] int NOT NULL,
CONSTRAINT [PK_Term] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Term_Schools_SchoolId] FOREIGN KEY ([SchoolId])
REFERENCES [Schools] ([Id]) ON DELETE CASCADE);

Queries using DateOnly and TimeOnly work in the expected manner. For example, the
following LINQ query finds schools that are currently open:

C#

openSchools = await context.Schools


.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours.Any(
o => o.DayOfWeek == dayOfWeek
&& o.OpensAt < time && o.ClosesAt >= time))
.ToListAsync();

This query translates to the following SQL, as shown by ToQueryString:

SQL

DECLARE @__today_0 date = '2023-02-07';


DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '19:53:40.4798052';

SELECT [s].[Id], [s].[Founded], [s].[Name], [o0].[SchoolId], [o0].[Id],


[o0].[ClosesAt], [o0].[DayOfWeek], [o0].[OpensAt]
FROM [Schools] AS [s]
LEFT JOIN [OpeningHours] AS [o0] ON [s].[Id] = [o0].[SchoolId]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0 AND
[t].[LastDay] >= @__today_0) AND EXISTS (
SELECT 1
FROM [OpeningHours] AS [o]
WHERE [s].[Id] = [o].[SchoolId] AND [o].[DayOfWeek] = @__dayOfWeek_1 AND
[o].[OpensAt] < @__time_2 AND [o].[ClosesAt] >= @__time_2)
ORDER BY [s].[Id], [o0].[SchoolId]

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

Name Farr High School

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#

openSchools = await context.Schools


.Where(
s => s.Terms.Any(
t => t.FirstDay <= today
&& t.LastDay >= today)
&& s.OpeningHours[(int)dayOfWeek].OpensAt < time
&& s.OpeningHours[(int)dayOfWeek].ClosesAt >= time)
.ToListAsync();

This query translates to the following SQL, as shown by ToQueryString:

SQL

DECLARE @__today_0 date = '2023-02-07';


DECLARE @__dayOfWeek_1 int = 2;
DECLARE @__time_2 time = '20:14:34.7795877';

SELECT [s].[Id], [s].[Founded], [s].[Name], [s].[OpeningHours]


FROM [Schools] AS [s]
WHERE EXISTS (
SELECT 1
FROM [Term] AS [t]
WHERE [s].[Id] = [t].[SchoolId] AND [t].[FirstDay] <= @__today_0
AND [t].[LastDay] >= @__today_0)
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1
AS int) AS nvarchar(max)) + '].OpensAt') AS time) < @__time_2
AND CAST(JSON_VALUE([s].[OpeningHours],'$[' + CAST(CAST(@__dayOfWeek_1
AS int) AS nvarchar(max)) + '].ClosesAt') AS time) >= @__time_2

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)));

This update translates to the following SQL:

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)

Reverse engineer Synapse and Dynamics 365 TDS


EF8 reverse engineering (a.k.a. scaffolding from an existing database) now supports
Synapse Serverless SQL Pool and Dynamics 365 TDS Endpoint databases.

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.

Enhancements to Math translations


Generic math interfaces were introduced in .NET 7. Concrete types like double and
float implemented these interfaces adding new APIs mirroring the existing

functionality of Math and MathF.

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.

Checking for pending model changes


We've added a new dotnet ef command to check whether any model changes have
been made since the last migration. This can be useful in CI/CD scenarios to ensure you
or a teammate didn't forget to add a migration.

.NET CLI

dotnet ef migrations has-pending-model-changes


You can also perform this check programmatically in your application or tests using the
new dbContext.Database.HasPendingModelChanges() method.

Enhancements to SQLite scaffolding


SQLite only supports four primitive data types--INTEGER, REAL, TEXT, and BLOB.
Previously, this meant that when you reverse engineered a SQLite database to scaffold
an EF Core model, the resulting entity types would only included properties of type
long , double , string , and byte[] . Additional .NET types are supported by the EF Core

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

Column type name .NET type

BOOLEAN byte[] bool

SMALLINT long short

INT long int

BIGINT long

STRING byte[] string

ノ Expand table

Data format .NET type

'0.0' string decimal

'1970-01-01' string DateOnly

'1970-01-01 00:00:00' string DateTime

'00:00:00' string TimeSpan

'00000000-0000-0000-0000-000000000000' string Guid

Sentinel values and database defaults


Databases allow columns to be configured to generate a default value if no value is
provided when inserting a row. This can be represented in EF using HasDefaultValue for
constants:

C#

b.Property(e => e.Status).HasDefaultValue("Hidden");

Or HasDefaultValueSql for arbitrary SQL clauses:

C#

b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");

 Tip

The code shown below comes from DefaultConstraintSample.cs .

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#

b.Property(e => e.Credits).HasDefaultValueSql(10);

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#

b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);

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#

public class Person


{
public int Id { get; set; }
public int Credits { get; set; } = -1;
}

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(); .

Database defaults for booleans


Boolean properties present an extreme form of this problem, since the CLR default
( false ) is one of only two valid values. This means that a bool property with a database
default constraint will only have a value inserted if that value is true . When the
database default value is false , then this means when the property value is false , then
the database default will be used, which is false . Otherwise, if the property value is
true , then true will be inserted. So when the database default is false , then the

database column ends up with the correct value.

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.

Database defaults for enums


Enum properties can have similar problems to bool properties because enums typically
have a very small set of valid values, and the CLR default may be one of these values. For
example, consider this entity type and enum:

C#

public class Course


{
public int Id { get; set; }
public Level Level { get; set; }
}

public enum Level


{
Beginner,
Intermediate,
Advanced,
Unspecified
}

The Level property is then configured with a database default:

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#

public enum Level


{
Unspecified,
Beginner,
Intermediate,
Advanced
}

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#

public class Course


{
public int Id { get; set; }
public Level Level { get; set; } = Level.Unspecified;
}

Using a nullable backing field


A more general way to handle the problem described above is to create a nullable
backing field for the non-nullable property. For example, consider the following entity
type with a bool property:
C#

public class Account


{
public int Id { get; set; }
public bool IsActive { get; set; }
}

The property can be given a nullable backing field:

C#

public class Account


{
public int Id { get; set; }

private bool? _isActive;

public bool IsActive


{
get => _isActive ?? false;
set => _isActive = value;
}
}

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.

Better ExecuteUpdate and ExecuteDelete


SQL commands that perform updates and deletes, such as those generated by
ExecuteUpdate and ExecuteDelete methods, must target a single database table.

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#

public class Customer


{
public int Id { get; set; }
public required string Name { get; set; }
public required CustomerInfo CustomerInfo { get; set; }
}

[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]

As a final example, in EF8, ExecuteUpdate can be used to update entities in a TPT


hierarchy as long as all updated properties are mapped to the same table. For example,
consider these entity types mapped using TPT:

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; }
}

With EF8, the Note property can be updated:

C#

await context.TptSpecialCustomers
.Where(e => e.Name == name)
.ExecuteUpdateAsync(s => s.SetProperty(b => b.Note, "Noted"));

Or the Name property can be updated:

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

Throws the following exception:

text

The LINQ expression 'DbSet<SpecialCustomerTpt>()


.Where(s => s.Name == __name_0)
.ExecuteUpdate(s => s.SetProperty<string>(
propertyExpression: b => b.Note,
valueExpression: "Noted").SetProperty<string>(
propertyExpression: b => b.Name,
valueExpression: b => b.Name + " (Noted)"))' could not be
translated. Additional information: Multiple 'SetProperty' invocations refer
to different tables ('b => b.Note' and 'b => b.Name'). A single
'ExecuteUpdate' call can only update the columns of a single table. See
https://go.microsoft.com/fwlink/?linkid=2101038 for more information.

Better use of IN queries


When the Contains LINQ operator is used with a subquery, EF Core now generates
better queries using SQL IN instead of EXISTS ; aside from producing more readable
SQL, in some cases this can result in dramatically faster queries. For example, consider
the following LINQ query:

C#

var blogsWithPosts = await context.Blogs


.Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
.ToListAsync();

EF7 generates the following for PostgreSQL:

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

SELECT b."Id", b."Name"


FROM "Blogs" AS b
WHERE b."Id" IN (
SELECT p."BlogId"
FROM "Posts" AS p
)

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.

Numeric rowversions for SQL Azure/SQL Server


SQL Server automatic optimistic concurrency is handled using rowversion columns. A
rowversion is an 8-byte opaque value passed between database, client, and server. By

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();

Translates to the following Azure SQL when using EF7:

SQL

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]


FROM [Customers] AS [c]
WHERE ((([c].[Id] * 3) + 2) > 0 AND ([c].[FirstName] IS NOT NULL)) OR ([c].
[LastName] IS NOT NULL)

Which has been improved to the following when using EF8:

SQL

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]


FROM [Customers] AS [c]
WHERE ([c].[Id] * 3 + 2 > 0 AND [c].[FirstName] IS NOT NULL) OR [c].
[LastName] IS NOT NULL

Specific opt-out for RETURNING/OUTPUT


clause
EF7 changed the default update SQL to use RETURNING / OUTPUT for fetching back
database-generated columns. Some cases where identified where this does not work,
and so EF8 introduces explicit opt-outs for this behavior.

For example, to opt-out of OUTPUT when using the SQL Server/Azure SQL provider:

C#

modelBuilder.Entity<Customer>().ToTable(tb =>
tb.UseSqlOutputClause(false));

Or to opt-out of RETURNING when using the SQLite provider:


C#

modelBuilder.Entity<Customer>().ToTable(tb =>
tb.UseSqlReturningClause(false));

Other minor changes


In addition to the enhancements described above, there have been many smaller
changes made to EF8. This includes:

NativeAOT/trimming compatibility for Microsoft.Data.Sqlite


Allow Multi-region or Application Preferred Regions in EF Core Cosmos
SQLite: Add EF.Functions.Unhex
SQL Server Index options SortInTempDB and DataCompression
Allow 'unsharing' connection between contexts
Add Generic version of EntityTypeConfiguration Attribute
Query: add support for projecting JSON entities that have been composed on
Remove unneeded subquery and projection when using ordering without
limit/offset in set operations
Allow pooling DbContext with singleton services
Optional RestartSequenceOperation.StartValue
Allow UseSequence and HiLo on non-key properties
Provide more information when 'No DbContext was found' error is generated
Pass query tracking behavior to materialization interceptor
Use case-insensitive string key comparisons on SQL Server
Allow value converters to change the DbType
Resolve application services in EF services
Allow transfer of ownership of DbConnection from application to DbContext

You might also like