Upgrading to .NET 11: 9 Breaking Changes and Fixes
Last updated: October 24, 2026 · Tested with .NET 11 RC2 (SDK 11.0.100-rc.2.26504.105) against .NET 10.0.12 (SDK 10.0.401), Windows 11
Short answer: most projects move to net11.0 with a TFM change, but nine things broke or changed in my test projects. Four stop the build (SetCompatibilityVersion, the Microsoft.OpenApi 3.x types, the removed ConcurrencyLimiter package, and Newtonsoft.Json disappearing from test projects), two change how the app fails at runtime (a crashing BackgroundService now crashes the process, and Entra ID logins to SQL need a new package), and three silently return different values (DateOnly.TryParse, Math.Round with digits, and casts from double to small integer types). Below is each one with the exact error, the code that triggers it and the fix I verified.
| # | What you will see | Cause | Fix |
|---|---|---|---|
| 1 | CS0246: The type or namespace name 'Newtonsoft' could not be found in a test project | Microsoft.NET.Test.Sdk 18.8.0 no longer brings Newtonsoft.Json (also on .NET 10) | Add Newtonsoft.Json as a direct package |
| 2 | CS0103: The name 'CompatibilityVersion' does not exist | MVC compatibility APIs removed | Delete SetCompatibilityVersion(...) |
| 3 | CS0266: Cannot implicitly convert ... IOpenApiMediaType | Microsoft.AspNetCore.OpenApi now uses Microsoft.OpenApi 3.x | Use the interface types in transformers |
| 4 | NU1102: Unable to find package Microsoft.AspNetCore.ConcurrencyLimiter | Package not published for .NET 11 | Move to AddConcurrencyLimiter in the rate limiter |
| 5 | Entra ID (Authentication=Active Directory Default) SQL logins fail | EF Core 11 pulls Microsoft.Data.SqlClient 7.0 without the Azure providers | Add Microsoft.Data.SqlClient.Extensions.Azure |
| 6 | Worker process exits with an unhandled exception instead of code 0 | Host rethrows a failed BackgroundService | Intended; catch it only if you must |
| 7 | ArgumentException from DateOnly.TryParse | Unsupported DateTimeStyles now throw | Pass only whitespace styles |
| 8 | Math.Round(2.675, 2) returns 2.67 instead of 2.68 | Rounding uses the exact binary value | Use decimal for money |
| 9 | (byte)298.0 returns 255 instead of 42 | Casts to small integer types saturate | Decide which result you want and write it explicitly |
How I Tested It
Every project builds for both versions from the same source. A Directory.Build.props switches the target framework and the Microsoft.Extensions/ASP.NET Core package versions with one property:
<TF Condition="'$(TF)' == ''">net10.0</TF>
<TargetFramework>$(TF)</TargetFramework>
<ExtVersion Condition="'$(TF)' == 'net10.0'">10.0.13</ExtVersion>
<ExtVersion Condition="'$(TF)' == 'net11.0'">11.0.0-rc.2.26504.105</ExtVersion>
The “before” runs used the .NET 10 SDK (dotnet run -p:TF=net10.0), the “after” runs the .NET 11 RC2 SDK (dotnet run -p:TF=net11.0). All errors and outputs below are copied from those runs.
1. Newtonsoft.Json Is Gone from Test Projects (CS0246)
This one is not tied to .NET 11 at all, which is why it surprises people. Up to version 18.7.0, Microsoft.NET.Test.Sdk pulled Newtonsoft.Json in as a transitive dependency, so test code could use JsonConvert without a package reference. From 18.8.0 it doesn’t. A test like this:
using Newtonsoft.Json;
public class OrderJsonTests
{
[Fact]
public void Order_round_trips_through_json()
{
var json = JsonConvert.SerializeObject(new { Id = 42, Total = 19.99m });
dynamic back = JsonConvert.DeserializeObject(json)!;
Assert.Equal(42, (int)back.Id);
}
}
passes with Test SDK 18.7.0 on both net10.0 and net11.0, and fails to build with 18.8.0 on both:
OrderJsonTests.cs(1,7): error CS0246: The type or namespace name 'Newtonsoft' could not be found (are you missing a using directive or an assembly reference?)
So you can hit this on .NET 10 the day Dependabot bumps the test SDK. The xUnit template in the .NET 11 RC2 SDK still references 17.14.1, so a new project is fine until you update packages. The fix is a direct reference (I tested 13.0.4, the test passes again):
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" Condition="'$(AddNewtonsoft)' == 'true'" />
(In a normal project, drop the Condition; I used it to switch between the failing and the fixed build.) If you are moving off Newtonsoft anyway, see System.Text.Json vs Newtonsoft: .NET 10 migration gotchas.
2. SetCompatibilityVersion No Longer Compiles (CS0103, CS1061)
Old ASP.NET Core projects often still carry this line from the 2.x days:
builder.Services.AddControllers()
.SetCompatibilityVersion(CompatibilityVersion.Latest);
On .NET 10 it was only a warning (ASP5001, “This API is obsolete and will be removed in a future version”). On .NET 11 the API is gone:
Program.cs(8,30): error CS0103: The name 'CompatibilityVersion' does not exist in the current context
Program.cs(8,6): error CS1061: 'IMvcBuilder' does not contain a definition for 'SetCompatibilityVersion' and no accessible extension method 'SetCompatibilityVersion' accepting a first argument of type 'IMvcBuilder' could be found (are you missing a using directive or an assembly reference?)
It hasn’t done anything since ASP.NET Core 3.0, so the fix is to delete it:
builder.Services.AddControllers();
3. OpenAPI Transformers: Microsoft.OpenApi 3.x (CS0266)
Microsoft.AspNetCore.OpenApi 10.0.13 resolved Microsoft.OpenApi 2.12.2; the 11.0 RC2 package resolves 3.10.2. I expected most transformer code to break. It didn’t: adding a security scheme, setting Extensions with JsonNodeExtension and touching schemas compiled unchanged. What broke was building response content, because Content is now a dictionary of the interface type:
operation.Responses["429"] = new OpenApiResponse
{
Description = "Too many requests",
Content = new Dictionary<string, OpenApiMediaType>
{
["application/problem+json"] = new OpenApiMediaType()
}
};
Program.cs(31,23): error CS0266: Cannot implicitly convert type 'System.Collections.Generic.Dictionary<string, Microsoft.OpenApi.OpenApiMediaType>' to 'System.Collections.Generic.IDictionary<string, Microsoft.OpenApi.IOpenApiMediaType>'. An explicit conversion exists (are you missing a cast?)
The fix is one type argument:
Content = new Dictionary<string, IOpenApiMediaType>
{
["application/problem+json"] = new OpenApiMediaType()
}
While you are there: schema.Example is obsolete (CS0618: 'OpenApiSchema.Example' is obsolete: 'Use Examples instead.'), replace it with schema.Examples = [new JsonObject { ... }]. Two output changes I saw in the generated document with no code change: the version line went from "openapi": "3.1.1" to "openapi": "3.2.0", and the server URL lost its trailing slash (http://localhost:5310/ on .NET 10, http://localhost:5311 on .NET 11). If a client generator or a contract test pins either, update it.
4. ConcurrencyLimiter Is Gone (NU1102)
If your project references the package with the framework version, restore fails on .NET 11 because there is no 11.x package:
error NU1102: Unable to find package Microsoft.AspNetCore.ConcurrencyLimiter with version (>= 11.0.0-rc.2.26504.105)
Pinning the package to 10.0.13 still builds and serves requests on .NET 11 (with the same CS0618 obsolete warning as on .NET 10), so that buys time. The real fix is the concurrency limiter in the rate-limiting middleware:
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status503ServiceUnavailable;
options.AddConcurrencyLimiter("concurrency", limiter =>
{
limiter.PermitLimit = 2;
limiter.QueueLimit = 25;
limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/", async () =>
{
await Task.Delay(500);
return "Hello World!";
}).RequireRateLimiting("concurrency");
app.Run();
Gotcha: the sample on Microsoft’s breaking-change page doesn’t have using Microsoft.AspNetCore.RateLimiting;. Without it, AddConcurrencyLimiter fails with CS1061: 'RateLimiterOptions' does not contain a definition for 'AddConcurrencyLimiter'. With it, six parallel requests to a 500 ms endpoint finish two at a time, which is the limit working:
request 1: 200 Hello World! after 1.74s
request 2: 200 Hello World! after 1.74s
request 3: 200 Hello World! after 0.73s
request 4: 200 Hello World! after 0.73s
request 5: 200 Hello World! after 1.23s
request 6: 200 Hello World! after 1.23s
The other limiters (fixed window, sliding window, token bucket) are compared in ASP.NET Core rate limiting: 4 algorithms compared.
5. EF Core 11 and Entra ID SQL Logins
EF Core 10’s SQL Server provider pulled in Microsoft.Data.SqlClient 6.0; EF Core 11 RC2 pulls in 7.0, which no longer contains the Entra ID (Azure AD) authentication providers. If your connection string uses Authentication=Active Directory Default (or another Active Directory * value), the login needs a provider that isn’t there anymore. I asked SqlClient directly which provider handles that method:
var provider = SqlAuthenticationProvider.GetProvider(SqlAuthenticationMethod.ActiveDirectoryDefault);
Console.WriteLine($"Provider for ActiveDirectoryDefault: {provider?.GetType().FullName ?? "(none registered)"}");
### net10.0 (EF Core 10.0.13)
Microsoft.Data.SqlClient 6.0.0.0
Provider for ActiveDirectoryDefault: Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider
### net11.0 (EF Core 11 RC2)
Microsoft.Data.SqlClient 7.0.0.0
Provider for ActiveDirectoryDefault: (none registered)
### net11.0 + Microsoft.Data.SqlClient.Extensions.Azure 7.1.1
Microsoft.Data.SqlClient 7.0.0.0
Provider for ActiveDirectoryDefault: Microsoft.Data.SqlClient.ActiveDirectoryAuthenticationProvider
The fix is the separate package. Microsoft’s page shows Version="1.0.0"; the current version on NuGet is 7.1.1, which is what I tested:
<PackageReference Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="7.1.1" />
SQL logins with a user name and password, and Windows authentication, don’t need it. I couldn’t reproduce the exact runtime error text without an Azure SQL database, so search your connection strings for Active Directory before you deploy rather than waiting for the error in production.
6. A Failing BackgroundService Now Crashes the Process
This worker throws from ExecuteAsync:
class ImportWorker : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Delay(200, stoppingToken);
throw new InvalidOperationException("Import file not found: orders.csv");
}
}
With the default BackgroundServiceExceptionBehavior.StopHost, both versions log the failure and stop the host. The difference is what await host.RunAsync() does next:
### net10.0 -> exit code 0
RunAsync completed normally
### net11.0 -> exit code 127
Unhandled exception. System.InvalidOperationException: Import file not found: orders.csv
On .NET 10 the app exited with code 0 after a fatal error. On .NET 11 the exception is rethrown, so the process ends with a failure code: 127 is what Git Bash shows, the real Windows exit code was 0xE0434352 (an unhandled .NET exception). With two failing services you get an AggregateException:
Unhandled exception. System.AggregateException: One or more hosted services failed to stop or one or more background services threw an exception. (Import file not found: orders.csv) (Cleanup timed out)
This is a fix, not a bug: Kubernetes, systemd and Windows services can now see that the worker failed and restart it. If something depends on exit code 0 (a scheduled task that alerts on failure, for example, would now alert), wrap the call:
try
{
await host.RunAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Caught from RunAsync: {ex.GetType().Name}: {ex.Message}");
return 0;
}
For jobs that should retry instead of stopping the host, see Hangfire vs Quartz.NET vs BackgroundService.
7. DateOnly and TimeOnly TryParse Throw ArgumentException
DateOnly and TimeOnly only support the whitespace styles of DateTimeStyles. Passing anything else, such as AssumeUniversal copied from DateTime code, used to make TryParse return false for every input. Now it throws:
bool ok = DateOnly.TryParseExact("2026-10-23", "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal, out DateOnly d);
.NET 10: returned False, value 01/01/0001
.NET 11: ArgumentException: The only allowed values for the styles are AllowWhiteSpaces, AllowTrailingWhite, AllowLeadingWhite, and AllowInnerWhite. (Parameter 'style')
TimeOnly.TryParse with DateTimeStyles.AdjustToUniversal behaves the same way. Invalid user input with a valid style still just returns false, so this only hits code that passed the wrong flags, and that code never parsed anything successfully. Use DateTimeStyles.None or the whitespace flags. (Microsoft’s page shows a different message for this exception; the one above is what RC2 actually throws.)
8. Math.Round With Digits Returns Different Values
Math.Round(double, digits) used to scale by 10^digits, round, and scale back. .NET 11 rounds the exact binary value of the input. 2.675 is stored as 2.67499999…, so the correct answer to two places is 2.67:
.NET 10:
Math.Round(655.925, 2, AwayFromZero) = 655.93
Math.Round(2.675, 2, AwayFromZero) = 2.68
ArgumentOutOfRangeException: Rounding digits must be between 0 and 15, inclusive. (Parameter 'digits')
.NET 11:
Math.Round(655.925, 2, AwayFromZero) = 655.92
Math.Round(2.675, 2, AwayFromZero) = 2.67
Math.Round(1.5, 16) = 1.5
How often does it matter? I rounded 100,000 random three-decimal values (prices like 12.345) to two places and compared with the old Math.Round(v * 100) / 100:
3-decimal values where Round(v,2) differs from Round(v*100)/100: 4298 of 100000
So about 4.3% of results change. If a test asserts a rounded double or a report compares totals, expect failures. For money, use decimal, which isn’t affected and gives the answer people expect: Math.Round(1.005m, 2, MidpointRounding.AwayFromZero) is 1.01 on both versions.
9. Casting double to byte or short Saturates
Unchecked casts from double to byte, sbyte, short, ushort and char now clamp to the type’s range instead of keeping the low bits:
.NET 10:
x = 298 (byte)42 (short)298 (ushort)298 (sbyte)42
x = -42 (byte)214 (short)-42 (ushort)65494 (sbyte)-42
x = 65578 (byte)42 (short)42 (ushort)42 (sbyte)42
.NET 11:
x = 298 (byte)255 (short)298 (ushort)298 (sbyte)127
x = -42 (byte)0 (short)-42 (ushort)0 (sbyte)-42
x = 65578 (byte)255 (short)32767 (ushort)65535 (sbyte)127
Values in range are unchanged, so this only matters for code that overflowed on purpose, like image or audio processing that relied on wrap-around. To keep the old result, convert through int first:
Console.WriteLine($"Old behaviour kept explicitly: unchecked((byte)(int)298.0) = {unchecked((byte)(int)Cast.Id(298.0))}");
Old behaviour kept explicitly: unchecked((byte)(int)298.0) = 42
(Cast.Id in my test is a [MethodImpl(MethodImplOptions.NoInlining)] identity method, so the JIT converts at run time instead of folding the constant at compile time. In your code it is simply unchecked((byte)(int)value).)
Upgrade Checklist
- Search for
SetCompatibilityVersionandUseConcurrencyLimiter; delete the first, replace the second. - Build the OpenAPI transformers; fix
Contentdictionaries and replaceExamplewithExamples. - In test projects, add
Newtonsoft.Jsonif anything usesJsonConvertorJObject. - Search connection strings for
Active Directory; addMicrosoft.Data.SqlClient.Extensions.Azureif found. - Check what runs your workers and how it treats a non-zero exit code.
- Search for
DateOnly.TryParse/TimeOnly.TryParsewith aDateTimeStylesargument. - Run the tests: rounding and overflow changes show up as wrong values, not build errors.
Microsoft’s full list, including changes I didn’t hit (tar and zip checksum validation, named mutex scope on Unix, Kestrel protocol checks): Breaking changes in .NET 11 and Breaking changes in ASP.NET Core 11. For the new language features in the same release, see C# 15 union types.
Related: Moq in C#: setup, verify and common errors · IExceptionHandler in ASP.NET Core · all ASP.NET Core tutorials · all C# tutorials