
Migrate Dotnet8 To Dotnet9
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
migrate-dotnet8-to-dotnet9 is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- migrate-dotnet8-to-dotnet9
- AI & Agent Building
- AI-coding skill
Migrate Dotnet8 To Dotnet9 by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,886 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill migrate-dotnet8-to-dotnet9Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
.NET 8 → .NET 9 Migration
Migrate a .NET 8 project or solution to .NET 9, systematically resolving all breaking changes. The outcome is a project targeting net9.0 that builds cleanly, passes tests, and accounts for every behavioral, source-incompatible, and binary-incompatible change introduced in the .NET 9 release.
When to Use
- Upgrading
TargetFrameworkfromnet8.0tonet9.0 - Resolving build errors or new warnings after updating the .NET 9 SDK
- Adapting to behavioral changes in .NET 9 runtime, ASP.NET Core 9, or EF Core 9
- Replacing
BinaryFormatterusage (now always throws at runtime) - Updating CI/CD pipelines, Dockerfiles, or deployment scripts for .NET 9
When Not to Use
- The project already targets
net9.0and builds cleanly — migration is done. If the goal is to reachnet10.0, use themigrate-dotnet9-to-dotnet10skill as the next step. - Upgrading from .NET 7 or earlier — address the prior version breaking changes first
- Migrating from .NET Framework — that is a separate, larger effort
- Greenfield projects that start on .NET 9 (no migration needed)
Inputs
| Input | Required | Description |
|---|---|---|
| Project or solution path | Yes | The .csproj, .sln, or .slnx entry point to migrate |
| Build command | No | How to build (e.g., dotnet build, a repo build script). Auto-detect if not provided |
| Test command | No | How to run tests (e.g., dotnet test). Auto-detect if not provided |
| Project type hints | No | Whether the project uses ASP.NET Core, EF Core, WinForms, WPF, containers, etc. Auto-detect from PackageReferences and SDK attributes if not provided |
Workflow
Answer directly from the loaded reference documents. Do not search the filesystem or fetch web pages for breaking change information — the references contain the authoritative details. Focus on identifying which breaking changes apply and providing concrete fixes.
>
Commit strategy: Commit at each logical boundary — after updating the TFM (Step 2), after resolving build errors (Step 3), after addressing behavioral changes (Step 4), and after updating infrastructure (Step 5). This keeps each commit focused and reviewable.
Step 1: Assess the project
1. Identify how the project is built and tested. Look for build scripts, .sln/.slnx files, or individual .csproj files. 2. Run dotnet --version to confirm the .NET 9 SDK is installed. If it is not, stop and inform the user. 3. Determine which technology areas the project uses by examining:
- SDK attribute:
Microsoft.NET.Sdk.Web→ ASP.NET Core;Microsoft.NET.Sdk.WindowsDesktopwith<UseWPF>or<UseWindowsForms>→ WPF/WinForms - PackageReferences:
Microsoft.EntityFrameworkCore.*→ EF Core;Microsoft.Extensions.Http→ HttpClientFactory - Dockerfile presence → Container changes relevant
- P/Invoke or native interop usage → Interop changes relevant
- `BinaryFormatter` usage → Serialization migration needed
- `System.Text.Json` usage → Serialization changes relevant
- X509Certificate constructors → Cryptography changes relevant
4. Record which reference documents are relevant (see the reference loading table in Step 3). 5. Do a clean build (dotnet build --no-incremental or delete bin/obj) on the current net8.0 target to establish a clean baseline. Record any pre-existing warnings.
Step 2: Update the Target Framework
1. In each .csproj (or Directory.Build.props if centralized), change:
<TargetFramework>net8.0</TargetFramework>to:
<TargetFramework>net9.0</TargetFramework>For multi-targeted projects, add net9.0 to <TargetFrameworks> or replace net8.0.
2. Update all Microsoft.Extensions.*, Microsoft.AspNetCore.*, Microsoft.EntityFrameworkCore.*, and other Microsoft package references to their 9.0.x versions. If using Central Package Management (Directory.Packages.props), update versions there.
3. Run dotnet restore. Watch for:
- Version requirements: .NET 9 SDK requires Visual Studio 17.12+ to target
net9.0(17.11 fornet8.0and earlier). - New warnings for .NET Standard 1.x and .NET 7 targets — consider updating or removing outdated target frameworks.
4. Run a clean build. Collect all errors and new warnings. These will be addressed in Step 3.
Step 3: Resolve build errors and source-incompatible changes
Work through compilation errors and new warnings systematically. Load the appropriate reference documents based on the project type:
| If the project uses… | Load reference |
|---|---|
| Any .NET 9 project | references/csharp-compiler-dotnet8to9.md |
| Any .NET 9 project | references/core-libraries-dotnet8to9.md |
| Any .NET 9 project | references/sdk-msbuild-dotnet8to9.md |
| ASP.NET Core | references/aspnet-core-dotnet8to9.md |
| Entity Framework Core | references/efcore-dotnet8to9.md |
| Cryptography APIs | references/cryptography-dotnet8to9.md |
| System.Text.Json, HttpClient, networking | references/serialization-networking-dotnet8to9.md |
| Windows Forms or WPF | references/winforms-wpf-dotnet8to9.md |
| Docker containers, native interop | references/containers-interop-dotnet8to9.md |
| Runtime configuration, deployment | references/deployment-runtime-dotnet8to9.md |
Common source-incompatible changes to check for:
1. `params` span overload resolution — New params ReadOnlySpan<T> overloads on String.Join, String.Concat, Path.Combine, Task.WhenAll, and many more now bind preferentially. Code calling these methods inside Expression lambdas will fail (CS8640/CS9226). See references/core-libraries-dotnet8to9.md.
2. `StringValues` ambiguous overload — The params Span<T> feature creates ambiguity with StringValues implicit operators on methods like String.Concat, String.Join, Path.Combine. Fix by explicitly casting arguments. See references/core-libraries-dotnet8to9.md.
3. New obsoletion warnings (SYSLIB0054–SYSLIB0057):
SYSLIB0054: ReplaceThread.VolatileRead/VolatileWritewithVolatile.Read/Volatile.WriteSYSLIB0057: ReplaceX509Certificate2/X509Certificatebinary/file constructors withX509CertificateLoadermethods- Also
SYSLIB0055(ARM AdvSimd signed overloads) andSYSLIB0056(Assembly.LoadFrom with hash algorithm) — seereferences/core-libraries-dotnet8to9.md
4. C# 13 `InlineArray` on record structs — [InlineArray] attribute on record struct types is now disallowed (CS9259). Change to a regular struct. See references/csharp-compiler-dotnet8to9.md.
5. C# 13 iterator safe context — Iterators now introduce a safe context in C# 13. Local functions inside iterators that used unsafe code inherited from an outer unsafe class will now error. Add unsafe modifier to the local function. See references/csharp-compiler-dotnet8to9.md.
6. C# 13 collection expression overload resolution — Empty collection expressions ([]) no longer use span vs non-span to tiebreak overloads. Exact element type is now preferred. See references/csharp-compiler-dotnet8to9.md.
7. `String.Trim(params ReadOnlySpan<char>)` removed — Code compiled against .NET 9 previews that passes ReadOnlySpan<char> to Trim/TrimStart/TrimEnd must rebuild; the overload was removed in GA. See references/core-libraries-dotnet8to9.md.
8. `BinaryFormatter` always throws — If the project uses BinaryFormatter, stop and inform the user — this is a major decision. See references/serialization-networking-dotnet8to9.md.
9. `HttpListenerRequest.UserAgent` is nullable — The property is now string?. Add null checks. See references/serialization-networking-dotnet8to9.md.
10. Windows Forms nullability annotation changes — Some WinForms API parameters changed from nullable to non-nullable. Update call sites. See references/winforms-wpf-dotnet8to9.md.
11. Windows Forms security analyzers (WFO1000) — New analyzers produce errors for properties without explicit serialization configuration. See references/winforms-wpf-dotnet8to9.md.
Build again after each batch of fixes. Repeat until the build is clean.
Step 4: Address behavioral changes
Behavioral changes do not cause build errors but may change runtime behavior. Review each applicable item and determine whether the previous behavior was relied upon.
High-impact behavioral changes (check first):
1. Floating-point to integer conversions are now saturating — Conversions from float/double to integer types now saturate instead of wrapping on x86/x64. See references/deployment-runtime-dotnet8to9.md.
2. EF Core: Pending model changes exception — Migrate()/MigrateAsync() now throws if the model has pending changes. Search for `DateTime.Now`, `DateTime.UtcNow`, or `Guid.NewGuid()` in any `HasData` call — these must be replaced with fixed constants (e.g., new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)). See references/efcore-dotnet8to9.md.
3. EF Core: Explicit transaction exception — Migrate() inside a user transaction now throws. See references/efcore-dotnet8to9.md.
4. HttpClientFactory uses `SocketsHttpHandler` by default — Code that casts the primary handler to HttpClientHandler will get InvalidCastException. See references/serialization-networking-dotnet8to9.md.
5. HttpClientFactory header redaction by default — All header values in Trace-level logs are now redacted. See references/serialization-networking-dotnet8to9.md.
6. Environment variables take precedence over runtimeconfig.json — Runtime configuration settings from environment variables now override runtimeconfig.json. See references/deployment-runtime-dotnet8to9.md.
7. ASP.NET Core `ValidateOnBuild`/`ValidateScopes` in development — HostBuilder now enables DI validation in development by default. See references/aspnet-core-dotnet8to9.md.
Other behavioral changes to review (may cause runtime exceptions ⚠️ or subtle behavioral differences):
- ⚠️
FromKeyedServicesAttributeno longer injects non-keyed service fallback — throwsInvalidOperationException - ⚠️ Container images no longer install zlib — apps depending on system zlib will fail
- ⚠️ Intel CET is now enabled by default — non-CET-compatible native libraries may cause process termination
BigIntegernow has a maximum length of(2^31) - 1bitsJsonDocumentdeserialization of JSONnullnow returns non-nullJsonDocumentwithJsonValueKind.Nullinstead of C#nullSystem.Text.Jsonmetadata reader now unescapes metadata property namesZipArchiveEntrynames/comments now respect the UTF-8 flagIncrementingPollingCounterinitial callback is now asynchronousInMemoryDirectoryInfoprepends rootDir to filesRuntimeHelpers.GetSubArrayreturns a different typePictureBoxraisesHttpRequestExceptioninstead ofWebExceptionStatusStripuses a different default rendererIMsoComponentsupport is opt-inSafeEvpPKeyHandle.DuplicateHandleup-refs the handleHttpClientmetrics reportserver.portunconditionally- URI query strings redacted in HttpClient EventSource events and IHttpClientFactory logs
dotnet watchis incompatible with Hot Reload for old frameworks- WPF
GetXmlNamespaceMapsreturnsHashtableinstead ofString
Step 5: Update infrastructure
1. Dockerfiles: Update base images. Note that .NET 9 container images no longer install zlib. If your app depends on zlib, add RUN apt-get update && apt-get install -y zlib1g to your Dockerfile.
# Before
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
FROM mcr.microsoft.com/dotnet/aspnet:8.0
# After
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
FROM mcr.microsoft.com/dotnet/aspnet:9.02. CI/CD pipelines: Update SDK version references. If using global.json, update:
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
}
}Review the rollForward policy — if set to "disable" or "latestPatch", the SDK may not resolve correctly after upgrading. "latestFeature" (recommended) allows the SDK to roll forward to the latest 9.0.x feature band.
3. Visual Studio version: .NET 9 SDK requires VS 17.12+ to target net9.0. VS 17.11 can only target net8.0 and earlier.
4. Terminal Logger: dotnet build now uses Terminal Logger by default in interactive terminals. CI scripts that parse MSBuild console output may need --tl:off or MSBUILDTERMINALLOGGER=off.
5. `dotnet workload` output: Output format has changed. Update any scripts that parse workload command output.
6. .NET Monitor images: Tags simplified to version-only (affects container orchestration referencing specific tags).
Step 6: Verify
1. Run a full clean build: dotnet build --no-incremental 2. Run all tests: dotnet test 3. If the application is containerized, build and test the container image 4. Smoke-test the application, paying special attention to:
- BinaryFormatter usage (will throw at runtime)
- Floating-point to integer conversion behavior
- EF Core migration application
- HttpClientFactory handler casting and logging
- DI validation in development environment
- Runtime configuration settings (environment variable precedence)
5. Review the diff and ensure no unintended behavioral changes were introduced
Reference Documents
The references/ folder contains detailed breaking change information organized by technology area. Load only the references relevant to the project being migrated:
| Reference file | When to load |
|---|---|
references/csharp-compiler-dotnet8to9.md | Always (C# 13 compiler breaking changes — InlineArray on records, iterator safe context, collection expression overloads) |
references/core-libraries-dotnet8to9.md | Always (applies to all .NET 9 projects) |
references/sdk-msbuild-dotnet8to9.md | Always (SDK and build tooling changes) |
references/aspnet-core-dotnet8to9.md | Project uses ASP.NET Core |
references/efcore-dotnet8to9.md | Project uses Entity Framework Core |
references/cryptography-dotnet8to9.md | Project uses System.Security.Cryptography or X.509 certificates |
references/serialization-networking-dotnet8to9.md | Project uses BinaryFormatter, System.Text.Json, HttpClient, or networking APIs |
references/winforms-wpf-dotnet8to9.md | Project uses Windows Forms or WPF |
references/containers-interop-dotnet8to9.md | Project uses Docker containers or native interop (P/Invoke) |
references/deployment-runtime-dotnet8to9.md | Project uses runtime configuration, deployment, or has floating-point to integer conversions |
{
"version": "0.1.0",
"category": "Legacy",
"compatibility": "Requires a .NET repository being migrated across framework, SDK, or compatibility changes."
}
ASP.NET Core 9 Breaking Changes
These changes affect projects using ASP.NET Core (Microsoft.NET.Sdk.Web).
Behavioral Changes
HostBuilder enables ValidateOnBuild/ValidateScopes in development
Impact: Medium. In the development environment, ValidateOnBuild and ValidateScopes are now enabled by default when options haven't been set with UseDefaultServiceProvider. This means DI registration issues that were previously silent will now throw at startup in development.
Mitigation: Fix the DI registration issues (recommended), or disable validation:
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateOnBuild = false;
options.ValidateScopes = false;
});Middleware types with multiple constructors
Impact: Low. Middleware activation behavior has changed when a middleware type has multiple constructors. Previously the behavior was undefined; now it selects the most appropriate constructor.
Forwarded Headers Middleware ignores X-Forwarded-* headers from unknown proxies
Impact: Medium. The ForwardedHeadersMiddleware now ignores X-Forwarded-* headers from proxies not in the KnownProxies or KnownNetworks list.
Mitigation: Add your trusted proxy addresses to KnownProxies or KnownNetworks:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.KnownProxies.Add(IPAddress.Parse("10.0.0.1"));
});Warning: Do not clearKnownProxiesorKnownNetworksto accept forwarded headers from any source. This disables ASP.NET Core's protection against spoofedX-Forwarded-*headers and is unsafe for production. Only register the specific proxy addresses or network ranges your infrastructure uses.
Source-Incompatible Changes
Legacy Mono and Emscripten APIs not exported to global namespace
Impact: Low. Legacy Mono and Emscripten interop APIs are no longer exported to the global namespace in Blazor WebAssembly. Use the specific namespace imports.
DefaultKeyResolution.ShouldGenerateNewKey altered meaning
The meaning of DefaultKeyResolution.ShouldGenerateNewKey has changed. Review code that checks this property.
Dev cert export no longer creates folder
dotnet dev-certs https --export-path no longer automatically creates the target folder. Ensure the directory exists before exporting.
Containers and Interop Breaking Changes (.NET 9)
These changes affect Docker containers, native interop, and CET (Control-flow Enforcement Technology).
Containers
Container images no longer install zlib
Impact: Medium. .NET 9 container images no longer install zlib because the .NET Runtime now includes a statically linked zlib-ng. If your app has a direct dependency on the zlib system package, install it manually:
FROM mcr.microsoft.com/dotnet/aspnet:9.0
RUN apt-get update && apt-get install -y zlib1g.NET Monitor images simplified to version-only tags
Impact: Low. .NET Monitor container image tags have been simplified to version-only format. Update any container orchestration configuration that references specific tag formats.
Interop
CET supported by default
Impact: Medium (binary incompatible). apphost and singlefilehost are now compiled with the /CETCOMPAT flag, enabling Intel CET (Control-flow Enforcement Technology) hardware-enforced stack protection. This enhances security against ROP exploits but imposes restrictions on shared libraries:
- Libraries cannot set thread context to locations not on the shadow stack
- Libraries cannot use exception handlers that jump to unlisted continuation addresses
- Non-CET-compatible native libraries loaded via P/Invoke may cause process termination
Mitigation: If a native library is incompatible:
<!-- Opt out of CET in project file -->
<PropertyGroup>
<CETCompat>false</CETCompat>
</PropertyGroup>Warning: Disabling CET removes hardware-enforced control-flow integrity, reducing protection against ROP (return-oriented programming) and JOP (jump-oriented programming) exploits. Only disable CET after confirming the specific native library is incompatible, and re-enable it once the library is updated. Prefer per-application opt-out via Windows Security / group policy over a project-wide setting when possible.
Core .NET Libraries Breaking Changes (.NET 9)
These breaking changes affect all .NET 9 projects regardless of application type.
Source-Incompatible Changes
C# overload resolution prefers params span-type overloads
Impact: High. .NET 9 added params ReadOnlySpan<T> overloads to many core methods. C# 13 overload resolution prefers params Span<T>/params ReadOnlySpan<T> over params T[]. This causes errors inside Expression lambdas, which cannot contain ref struct types.
Affected methods include: String.Join, String.Concat, String.Format, String.Split, Path.Combine, Path.Join, Task.WhenAll, Task.WhenAny, Task.WaitAll, Console.Write, Console.WriteLine, StringBuilder.AppendFormat, StringBuilder.AppendJoin, CancellationTokenSource.CreateLinkedTokenSource, ImmutableArray.Create, Delegate.Combine, JsonArray constructors, JsonTypeInfoResolver.Combine, and more.
// BREAKS — CS8640/CS9226 inside Expression lambda
Expression<Func<string, string, string>> join =
(x, y) => string.Join("", x, y); // binds to ReadOnlySpan overloadFix: Pass an explicit array to force binding to the params T[] overload:
Expression<Func<string, string, string>> join =
(x, y) => string.Join("", new string[] { x, y });Ambiguous overload resolution affecting StringValues
Impact: Medium. StringValues (from Microsoft.Extensions.Primitives) has implicit operators for string and string[] that conflict with the new params Span<T> overloads. The compiler throws CS0121 for ambiguous calls.
Fix: Explicitly cast arguments to the appropriate type or use named parameters.
New TimeSpan.From*() overloads that take integers
Impact: Low (primarily F#). New integer overloads of TimeSpan.FromDays, FromHours, FromMinutes, etc. cause ambiguity in F# code. Specify the argument type to select the correct overload.
String.Trim(params ReadOnlySpan<char>) overload removed
Impact: Medium. The Trim, TrimStart, and TrimEnd overloads accepting ReadOnlySpan<char> were added in .NET 9 previews but removed in GA because they caused behavioral changes with common extension methods (e.g., "prefixinfixsuffix".TrimEnd("suffix") would change behavior).
Code compiled against .NET 9 previews that explicitly passes ReadOnlySpan<char> to these overloads may fail with MissingMethodException at runtime when run on the GA runtime, or fail to compile when retargeted to .NET 9 GA. Code using params char[] continues to work.
// BREAKS — compiled against .NET 9 Preview with ReadOnlySpan<char> overloads
static string TrimLogEntry(string str)
{
ReadOnlySpan<char> trimChars = [';', ',', '.'];
return str.Trim(trimChars); // calls Trim(ReadOnlySpan<char>) in previews only
}
// Fix — target GA and use char[] so Trim binds to existing overloads
static string TrimLogEntry(string str)
{
char[] trimChars = [';', ',', '.'];
return str.Trim(trimChars); // calls Trim(char[]) / Trim(params char[])
}Note: Assemblies compiled against .NET 9 Preview 6 through RC2 must be recompiled to avoid MissingMethodException at runtime.API obsoletions (SYSLIB0054–SYSLIB0057)
| Diagnostic | What's obsolete | Replacement |
|---|---|---|
| SYSLIB0054 | Thread.VolatileRead/Thread.VolatileWrite | Volatile.Read/Volatile.Write |
| SYSLIB0055 | AdvSimd.ShiftRightLogicalRoundedNarrowingSaturate* signed overloads | Unsigned overloads |
| SYSLIB0056 | Assembly.LoadFrom with AssemblyHashAlgorithm | Overloads without AssemblyHashAlgorithm |
| SYSLIB0057 | X509Certificate2/X509Certificate binary/file constructors and X509Certificate2Collection.Import | X509CertificateLoader methods |
These use custom diagnostic IDs — suppressing CS0618 does not suppress them.
New version of some OOB packages
Impact: Low. Some out-of-band packages have updated major versions. Review and update your package references.
Behavioral Changes
BinaryFormatter always throws
Impact: High. BinaryFormatter.Serialize and BinaryFormatter.Deserialize now always throw NotSupportedException regardless of any configuration. The EnableUnsafeBinaryFormatterSerialization AppContext switch has been removed. See serialization-networking-dotnet8to9.md for full details.
BigInteger maximum length restriction
Impact: Low. BigInteger is now limited to (2^31) - 1 bits (approximately 2.14 billion bits / ~256 MB). Values exceeding this throw OverflowException.
Default InlineArray Equals() and GetHashCode() throw
Equals() and GetHashCode() on types marked with [InlineArrayAttribute] now throw instead of returning incorrect results.
Inline array struct size limit is enforced
Impact: Low. [InlineArray] structs with a byte size exceeding 1 MiB (1,048,576 bytes) now fail to load at runtime.
Creating type of array of System.Void not allowed
Attempting to create typeof(void[]) or similar constructs now throws TypeLoadException.
EnumConverter validates registered types
EnumConverter now validates that the type passed is actually an enum, throwing for non-enum types.
FromKeyedServicesAttribute no longer injects non-keyed parameter
Impact: Medium. When [FromKeyedServices("key")] is used and the keyed service isn't registered, it no longer falls back to injecting a non-keyed service. Instead, InvalidOperationException is thrown.
IncrementingPollingCounter initial callback is asynchronous
The initial measurement callback for IncrementingPollingCounter is now invoked asynchronously.
InMemoryDirectoryInfo prepends rootDir to files
InMemoryDirectoryInfo now prepends the rootDir to file paths, which may change how matching works.
RuntimeHelpers.GetSubArray returns different type
RuntimeHelpers.GetSubArray may return a different concrete array type than before.
Support for empty environment variables
Empty environment variables are now supported and no longer treated as unset.
ZipArchiveEntry names and comments respect UTF8 flag
ZipArchiveEntry now correctly uses UTF-8 encoding for names and comments when the UTF-8 flag is set in the entry header, which may change how non-ASCII entry names are read.
Adding ZipArchiveEntry with CompressionLevel sets header flags
Adding a ZipArchiveEntry with an explicit CompressionLevel now sets the general-purpose bit flags in the ZIP central directory header.
Altered UnsafeAccessor support for non-open generics
UnsafeAccessor behavior changed for non-open generic types.
BinaryReader.ReadString() returns "\uFFFD" on malformed sequences
BinaryReader.ReadString() now returns the Unicode replacement character instead of throwing for malformed byte sequences.
Other behavioral changes (lower impact)
ServicePointManager(SYSLIB0014) is now fully obsolete — settings do not affectSslStreamorHttpClientAuthenticationManager(SYSLIB0009) methods now no-op or throwPlatformNotSupportedException
Cryptography Breaking Changes (.NET 9)
These changes affect projects using System.Security.Cryptography, X.509 certificates, or OpenSSL.
Source-Incompatible Changes
X509Certificate2 and X509Certificate constructors are obsolete (SYSLIB0057)
Impact: High. The constructors on X509Certificate and X509Certificate2 that accept content as byte[], ReadOnlySpan<byte>, or a file path are now obsolete. The Import methods on X509Certificate2Collection are also obsolete. Using them produces warning SYSLIB0057.
// BREAKS — SYSLIB0057 warning
var cert = new X509Certificate2(certBytes);
var cert2 = new X509Certificate2("cert.pfx", "password");
collection.Import(certBytes);Why: These APIs accepted multiple formats (X.509, PKCS7, PKCS12/PFX) from a single parameter, making it possible to load a different format than intended with user-supplied data.
Fix: Use X509CertificateLoader methods:
// Load DER/PEM encoded certificate
var derCert = X509CertificateLoader.LoadCertificate(certBytes);
// Load PFX/PKCS12
var pfxCert = X509CertificateLoader.LoadPkcs12(pfxBytes, "password");
// Load from file
var fileCert = X509CertificateLoader.LoadCertificateFromFile("cert.pem");
var filePfxCert = X509CertificateLoader.LoadPkcs12FromFile("cert.pfx", "password");APIs removed from System.Security.Cryptography.Pkcs netstandard2.0
Some APIs were removed from the netstandard2.0 target of the System.Security.Cryptography.Pkcs package. These APIs are still available when targeting .NET.
Behavioral Changes
SafeEvpPKeyHandle.DuplicateHandle up-refs the handle
Impact: Low. SafeEvpPKeyHandle.DuplicateHandle now increments the reference count on the underlying OpenSSL key handle instead of creating a fully independent copy. This is more efficient but means changes to one handle may affect duplicates.
Windows private key lifetime simplified
Impact: Low. The lifetime management of private keys associated with certificates on Windows has been simplified. This may affect code that makes assumptions about when private key handles are released.
C# 13 Compiler Breaking Changes (.NET 9)
These breaking changes are introduced by the Roslyn compiler shipping with the .NET 9 SDK. They affect all projects targeting net9.0 (which uses C# 13 by default). These are maintained separately from the runtime breaking changes at: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/breaking-changes/compiler%20breaking%20changes%20-%20dotnet%209
Source-Incompatible Changes
InlineArray attribute on record structs is disallowed
Impact: Medium. You can no longer apply the [InlineArray] attribute to record struct types. This produces error CS9259.
// BREAKS — CS9259 error
[System.Runtime.CompilerServices.InlineArray(10)]
record struct Buffer()
{
private int _element0;
}Fix: Change record struct to a plain struct:
[System.Runtime.CompilerServices.InlineArray(10)]
struct Buffer
{
private int _element0;
}Iterators introduce safe context in C# 13
Impact: Low–Medium. Although the language spec states that iterators introduce a safe context, Roslyn did not enforce this in C# 12 and lower. In C# 13, iterators always introduce a safe context, which can break scenarios where unsafe context was inherited by nested local functions.
// BREAKS in C# 13
unsafe class C
{
System.Collections.Generic.IEnumerable<int> M()
{
yield return 1;
local();
void local()
{
int* p = null; // error: unsafe code in safe context
}
}
}Fix: Add the unsafe modifier to the local function:
unsafe void local()
{
int* p = null; // OK
}Collection expression overload resolution changes
Impact: Low–Medium. Two changes in how collection expressions resolve overloads:
1. Empty collection expressions no longer use span to tiebreak: When passing [] to an overloaded method without a clear element type, ReadOnlySpan<T> vs Span<T> is no longer used to disambiguate. This can turn previously successful calls into errors.
class C
{
static void M(ReadOnlySpan<int> ros) {}
static void M(Span<object> s) {}
static void Main()
{
M([]); // Chose ReadOnlySpan<int> in C# 12, error in C# 13
}
}2. Exact element type is now preferred: Overload resolution prefers an exact element type match from expressions. This can change which overload is selected:
class C
{
static void M(ReadOnlySpan<byte> ros) {}
static void M(Span<int> s) {}
static void Main()
{
M([1]); // ReadOnlySpan<byte> in C# 12, Span<int> in C# 13
}
}Fix: Add explicit casts or use a typed variable to select the desired overload.
Default and params parameters considered in method group natural type
Impact: Low. The compiler previously inferred different delegate types depending on candidate order when default parameter values or params arrays were used. Now an ambiguity error is emitted. Fix by using explicit delegate types instead of var.
Other low-impact source changes
- `scoped` in lambda parameters (inherited from C# 12 changes): Always treated as a modifier in newer language versions.
- Indexers without `DefaultMemberAttribute`: No longer allowed (CS0656).
- `dotnet_style_require_accessibility_modifiers` now enforces on interface members consistently.
Deployment and Runtime Breaking Changes (.NET 9)
These changes affect runtime configuration, deployment, and JIT compiler behavior.
Deployment
Environment variables take precedence in app runtime configuration settings
Impact: Medium. When both an environment variable and a corresponding runtimeconfig.json setting are provided, the environment variable now takes precedence.
Example runtimeconfig.json:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
}Previously, this runtimeconfig.json setting would override DOTNET_gcServer=0. Now DOTNET_gcServer=0 overrides the config file, disabling server GC.
Mitigation: If your app runs in an environment with runtime configuration environment variables, either unset them or set them to the desired values. Ensure environment variables and config files are consistent.
Deprecated desktop Windows/macOS/Linux MonoVM runtime packages
Impact: Low. The desktop MonoVM runtime packages (Microsoft.NETCore.App.Runtime.Mono.*) are deprecated. Apps that explicitly used Mono on desktop should migrate to CoreCLR or use other supported runtimes.
JIT Compiler
Floating point to integer conversions are saturating
Impact: Medium. On x86/x64, conversions from float/double to integer types now use saturating behavior instead of the previous platform-specific wrapping:
| Scenario | .NET 8 (x86/x64) | .NET 9 |
|---|---|---|
(int)float.MaxValue | int.MinValue | int.MaxValue |
(int)float.NaN | int.MinValue | 0 |
(uint)(-1.0f) | Wrapping result | 0 |
(ulong)(double.MaxValue) | Wrapping result | ulong.MaxValue |
// .NET 8: (int)float.PositiveInfinity == int.MinValue (wrapping)
// .NET 9: (int)float.PositiveInfinity == int.MaxValue (saturating)
// .NET 8: (int)float.NaN == int.MinValue
// .NET 9: (int)float.NaN == 0
// .NET 8: (uint)(-1.0f) == some wrapping result
// .NET 9: (uint)(-1.0f) == 0Mitigation: If you relied on the previous wrapping behavior (which was already non-deterministic across platforms), use the new ConvertToIntegerNative<TInteger> methods on Single, Double, and Half for the fast, platform-native behavior. Or use platform-specific hardware intrinsics for exact control.
Some SVE APIs removed
A small number of ARM SVE (Scalable Vector Extension) APIs were removed in RC2 that were previously available in previews.
Entity Framework Core 9 Breaking Changes
These changes affect projects using EF Core.
High-Impact Changes
Exception is thrown when applying migrations if there are pending model changes
Impact: High. dotnet ef database update, Migrate(), and MigrateAsync() now throw if the model has pending changes compared to the last migration:
The model for context 'DbContext' has pending changes. Add a new migration before updating the database.
Common causes:
- No migrations exist at all (database managed through other means)
- Model snapshot is missing (migration was created manually)
- Non-deterministic model building (
DateTime.Now,Guid.NewGuid()inHasData) - Last migration created for a different provider
- ASP.NET Core Identity options that affect the model aren't applied in design-time factory
Non-deterministic `HasData` example (common pitfall):
// BREAKS — DateTime.UtcNow changes every evaluation, causing "pending model changes"
modelBuilder.Entity<Order>().HasData(
new Order { Id = 1, CreatedAt = DateTime.UtcNow });
// Fix — use a fixed constant
modelBuilder.Entity<Order>().HasData(
new Order { Id = 1, CreatedAt = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) });Mitigation (temporary workaround — not recommended for production):
// Suppress the warning if intentional — review before deploying to production,
// as this risks silent schema drift between the model and the database.
options.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));Exception is thrown when applying migrations in an explicit transaction
Impact: High. Migrate() and MigrateAsync() now manage their own transactions and ExecutionStrategy. Wrapping them in a user transaction throws:
A transaction was started before applying migrations. This prevents a database lock to be acquired.
// BREAKS
await dbContext.Database.CreateExecutionStrategy().ExecuteAsync(async () =>
{
await using var transaction = await dbContext.Database.BeginTransactionAsync(ct);
await dbContext.Database.MigrateAsync(ct);
await transaction.CommitAsync(ct);
});
// Fix — remove the external transaction
await dbContext.Database.MigrateAsync(ct);Mitigation (temporary workaround): If you need the explicit transaction:
// Suppress if you understand the transaction safety implications.
// EF Core manages its own transactions during migration; wrapping in a user
// transaction can prevent proper lock acquisition.
options.ConfigureWarnings(w => w.Ignore(RelationalEventId.MigrationsUserTransactionWarning));Medium-Impact Changes
Microsoft.EntityFrameworkCore.Design not found when using EF tools
Impact: Medium. Starting with .NET SDK 9.0.200, the EF tools may fail with: Could not load file or assembly 'Microsoft.EntityFrameworkCore.Design'. This is caused by a change in how private assets are included in .deps.json.
Mitigation: Mark the Design package as publishable:
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<Publish>true</Publish>
</PackageReference>Low-Impact Changes
EF.Functions.Unhex() now returns byte[]?
The return type annotation changed from byte[] to byte[]? to match SQLite's unhex function which returns NULL for invalid inputs. Add the null-forgiving operator (!) if you're certain the input is valid.
Compiled models reference value converter methods directly
Value converter methods referenced by compiled models must now be public or internal (not private), as the generated code references them directly for NativeAOT support.
SqlFunctionExpression nullability arguments arity validated
The number of arguments and nullability propagation arguments must now match. When in doubt, use false for the nullability argument.
ToString() returns empty string for null instances
ToString() on nullable value types in EF queries now consistently returns empty string when the value is null, matching C# behavior. Previously the result was inconsistent across types.
Shared framework dependencies updated to 9.0.x
EF Core 9.0 references 9.0.x versions of System.Text.Json, Microsoft.Extensions.Caching.Memory, etc. Apps targeting net8.0 will deploy these assemblies separately instead of using the shared framework.
Azure Cosmos DB Breaking Changes
EF Core 9.0 has extensive Cosmos DB changes. If using the Cosmos DB provider:
Discriminator property renamed to $type (High Impact)
The default discriminator property name changed from Discriminator to $type to align with System.Text.Json conventions. Existing documents use the old name.
Mitigation:
modelBuilder.Entity<Session>().HasDiscriminator<string>("Discriminator");id property no longer contains discriminator (High Impact)
The id property now contains only the EF key property value (e.g., 123), not EntityType|KeyValue (e.g., Product|123). Existing documents written by EF Core 8 still have the old compound format.
Implications for existing data:
FindAsyncand point-reads by id will fail because EF Core 9 generates123but the stored document hasProduct|123- New documents get the short format, creating id inconsistency with existing documents
- Direct Cosmos SQL queries matching on
c.idneed updating
Mitigation: Preserve the old id format so existing documents remain accessible:
modelBuilder.Entity<Product>().HasRootDiscriminatorInJsonId(true);JSON id property mapped to key (High Impact)
The JSON id property is now mapped directly to the entity key property.
Sync I/O no longer supported (Medium Impact)
Synchronous methods like SaveChanges() and ToList() on the Cosmos DB provider now throw. Use async equivalents.
SQL queries must project JSON values directly (Medium Impact)
Raw SQL queries against the Cosmos DB provider must now project values from the JSON document directly using VALUE.
Undefined results filtered from query results (Medium Impact)
Query results that are undefined in Cosmos DB are now automatically filtered out.
Incorrectly translated queries no longer translated (Medium Impact)
Some queries that were previously translated incorrectly now throw InvalidOperationException to prevent silent data corruption.
HasIndex now throws (Medium Impact)
HasIndex on a Cosmos DB entity type now throws InvalidOperationException at startup instead of being silently ignored. Remove all HasIndex calls for Cosmos entities — Cosmos DB indexing is managed through the container's indexing policy (Azure Portal, Bicep, or Terraform), not through EF Core.
IncludeRootDiscriminatorInJsonId renamed (Low Impact)
Renamed to HasRootDiscriminatorInJsonId after RC2.
SDK and MSBuild Breaking Changes (.NET 9)
These changes affect the .NET SDK, CLI tooling, NuGet, and MSBuild behavior.
Source-Incompatible Changes
Version requirements for .NET 9 SDK
The .NET 9 SDK has updated minimum Visual Studio and MSBuild version requirements:
- With VS 17.11, the .NET 9.0.100 SDK can only target
net8.0and earlier frameworks - VS 17.12 or later is required to target
net9.0
Attempting to target net9.0 in VS 17.11 produces: NETSDK1223: Targeting .NET 9.0 or higher in Visual Studio 2022 17.11 is not supported.
Warning emitted for .NET Standard 1.x target
Projects targeting .NET Standard 1.x now produce a build warning encouraging migration to a newer target.
Warning emitted for .NET 7 target
Projects targeting net7.0 now produce a build warning because .NET 7 is out of support. Update to net8.0 or net9.0.
New default RID used when targeting .NET Framework
A new default Runtime Identifier (RID) is used when targeting .NET Framework, which may affect build output paths.
Behavioral Changes
Terminal Logger is default
Impact: Medium for CI scripts. dotnet build and other build-related CLI commands now use Terminal Logger by default for interactive terminal sessions. Terminal Logger formats output differently from the console logger.
Mitigation:
- Per-command:
--tl:off - Global: set
MSBUILDTERMINALLOGGER=offenvironment variable
dotnet workload commands output change
dotnet workload commands have changed their output format. Scripts that parse workload command output may need updating.
dotnet sln add doesn't allow invalid file names
dotnet sln add now validates file names more strictly.
dotnet watch incompatible with Hot Reload for old frameworks
dotnet watch in .NET 9 SDK is not compatible with Hot Reload when targeting older frameworks. Use the SDK matching the target framework for dotnet watch scenarios.
installer repo version no longer documented
The installer repository version is no longer included in productcommits information.
MSBuild custom culture resource handling
MSBuild custom culture resource handling has changed (also applies to .NET 9 SDK 9.0.200/9.0.300).
Other behavioral changes
--interactivemay have different defaults in some scenarios- Default workload management may behave differently
Serialization and Networking Breaking Changes (.NET 9)
These changes affect projects using BinaryFormatter, System.Text.Json, HttpClient, and networking APIs.
Serialization
BinaryFormatter always throws
Impact: High. The in-box BinaryFormatter implementation now always throws NotSupportedException on Serialize and Deserialize, even with settings that previously enabled its use. The EnableUnsafeBinaryFormatterSerialization AppContext switch has been removed.
// Serialization — BREAKS at runtime with NotSupportedException
var formatter = new BinaryFormatter();
formatter.Serialize(stream, obj); // throws// Deserialization — BREAKS at runtime with NotSupportedException
var formatter = new BinaryFormatter();
var obj = formatter.Deserialize(stream); // throwsThis is a major architectural decision. If the project uses BinaryFormatter, the agent should stop and ask the user which replacement to use. Options include: 1. System.Text.Json — Modern, high-performance, built-in 2. MessagePack or protobuf-net — Compact binary formats 3. XmlSerializer or DataContractSerializer — XML-based formats
Security: Do not re-enable BinaryFormatter. A NuGet compatibility package exists (System.Runtime.Serialization.Formatters) but it is unsupported and inherently unsafe — BinaryFormatter deserialization enables remote code execution from crafted payloads, which is why it was removed. Do not suggest or use this package.The choice depends on existing data format, backward compatibility requirements, and performance needs. Stored data may need a migration strategy.
See the BinaryFormatter migration guide for detailed guidance.
Nullable JsonDocument properties deserialize to JsonValueKind.Null
Impact: Medium. Deserializing JSON null into JsonDocument now returns a non-null JsonDocument with RootElement.ValueKind == JsonValueKind.Null instead of returning C# null.
var doc = JsonSerializer.Deserialize<JsonDocument>("null");
// .NET 8: doc is null
// .NET 9: doc is not null, doc.RootElement.ValueKind == JsonValueKind.NullFix: Update code that checks doc is null to also check doc.RootElement.ValueKind:
if (doc is null || doc.RootElement.ValueKind == JsonValueKind.Null)
{
// handle null
}System.Text.Json metadata reader now unescapes metadata property names
Impact: Low. The JSON metadata reader now unescapes metadata property names (like $type, $id). This may affect custom converters or code that processes raw JSON metadata.
Networking
HttpClientFactory uses SocketsHttpHandler as primary handler
Impact: Medium. The default primary handler for HttpClientFactory-created clients is now SocketsHttpHandler instead of HttpClientHandler on platforms that support it. Code that casts the handler to HttpClientHandler will throw InvalidCastException.
// BREAKS — InvalidCastException at runtime in .NET 9
services.AddHttpClient("test")
.ConfigureHttpMessageHandlerBuilder(b =>
{
((HttpClientHandler)b.PrimaryHandler).UseCookies = false; // throws
});Fix options:
// Option 1: Explicitly configure a primary handler
services.AddHttpClient("test")
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false });
// Option 2: Check for both handler types
services.AddHttpClient("test")
.ConfigureHttpMessageHandlerBuilder(b =>
{
if (b.PrimaryHandler is HttpClientHandler hch) hch.UseCookies = false;
else if (b.PrimaryHandler is SocketsHttpHandler shh) shh.UseCookies = false;
});
// Option 3: Set defaults for all clients
services.ConfigureHttpClientDefaults(b =>
b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }));SocketsHttpHandler also has PooledConnectionLifetime preset to match HandlerLifetime, improving DNS rotation for captured clients.
HttpClientFactory logging redacts header values by default
Impact: Medium. All header values in Trace-level HttpClientFactory logs are now redacted by default. Previously, unspecified headers were logged in full.
Fix: Explicitly allowlist specific non-sensitive headers that need to be logged:
// Allow specific non-sensitive headers to be logged unredacted
services.ConfigureHttpClientDefaults(b =>
b.RedactLoggedHeaders(h => h != "Cache-Control" && h != "Accept"));Warning: Do not disable redaction globally withRedactLoggedHeaders(_ => false). This logs all header values in cleartext, includingAuthorizationtokens, cookies, and other credentials. Logs are often broadly accessible or exported to external systems, making this a credential leak risk.
HttpClient metrics report server.port unconditionally
HttpClient metrics now always include the server.port attribute, even for default ports (80/443). This may affect metric dashboards or alerting rules.
HttpListenerRequest.UserAgent is nullable
HttpListenerRequest.UserAgent is now string? instead of string. Add null checks.
URI query redaction in HttpClient EventSource events
URI query strings are now redacted in HttpClient EventSource events for security.
URI query redaction in IHttpClientFactory logs
URI query strings are now redacted in IHttpClientFactory logs for security.
Windows Forms and WPF Breaking Changes (.NET 9)
These changes affect projects using Windows Forms (<UseWindowsForms>true</UseWindowsForms>) or WPF (<UseWPF>true</UseWPF>).
Windows Forms
Source-Incompatible Changes
Changes to nullability annotations
Impact: Medium. Some WinForms API parameters changed nullability. Specifically, IWindowsFormsEditorService.DropDownControl(Control) parameter was previously nullable and is now non-nullable. Update implementations and call sites to match.
New security analyzers (WFO1000)
Impact: Medium. New analyzers enforce that properties in controls and UserControl objects have explicit serialization configuration via DesignerSerializationVisibilityAttribute, DefaultValueAttribute, or ShouldSerialize[PropertyName] methods. By default, the analyzer produces an error.
WFO1000: Property 'property' does not configure the code serialization for its property content.Fix: Add appropriate serialization attributes to flagged properties — DesignerSerializationVisibilityAttribute, DefaultValueAttribute, or a ShouldSerialize[PropertyName] method.
Warning: Do not suppress WFO1000 globally. This analyzer guards against insecure deserialization of control properties in the WinForms designer. Suppressing it can leave controls vulnerable to deserialization attacks through crafted designer files. If specific properties are intentionally excluded from serialization, use [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] on those properties rather than silencing the analyzer project-wide.Behavioral Changes
StatusStrip uses a different default renderer
Impact: Low. StatusStrip.RenderMode no longer defaults to ToolStripRenderMode.System. The visual appearance may differ. Set RenderMode explicitly to restore the previous look:
statusStrip.RenderMode = ToolStripRenderMode.System;Note: This change was reverted in a .NET 9 servicing release.
PictureBox raises HttpClient exceptions
Impact: Low. PictureBox now raises HttpRequestException and TaskCanceledException instead of WebException when loading images from URLs fails. Update catch blocks:
// Before
try { pictureBox.Load(url); }
catch (WebException) { }
// After
try { pictureBox.Load(url); }
catch (HttpRequestException) { }
catch (TaskCanceledException) { }IMsoComponent support is opt-in
Impact: Low. WinForms threads no longer automatically register with IMsoComponentManager instances. To restore:
<ItemGroup>
<RuntimeHostConfigurationOption Include="Switch.System.Windows.Forms.EnableMsoComponentManager" Value="true" />
</ItemGroup>BindingSource.SortDescriptions doesn't return null
SortDescriptions now returns an empty collection instead of null.
ComponentDesigner.Initialize throws ArgumentNullException
ComponentDesigner.Initialize now throws ArgumentNullException for null input.
DataGridViewRowAccessibleObject.Name starting row index
The starting row index in accessible object names has changed.
No exception if DataGridView is null
DataGridViewHeaderCell no longer throws NullReferenceException when DataGridView is null.
WPF
Behavioral Changes / Source-Incompatible
XmlNamespaceMaps type change
Impact: Low. The backing property of XmlAttributeProperties.XmlNamespaceMaps changed from String to Hashtable. The SetXmlNamespaceMaps method now accepts Hashtable instead of String.
// Before — passed string
XmlAttributeProperties.SetXmlNamespaceMaps(obj, someString);
// After — pass Hashtable
XmlAttributeProperties.SetXmlNamespaceMaps(obj, someHashtable);GetXmlNamespaceMaps now returns Hashtable instead of String.