What Is New in .NET 8
| Category | Highlights |
|---|---|
| New Features |
|
| Improvements |
|
| Breaking Changes |
|
| Deprecations |
|
What performance improvements does .NET 8 bring to production applications?
.NET 8 delivers meaningful, measurable throughput gains across the board without requiring any code changes for most apps. Dynamic profile-guided optimization (PGO) is now enabled by default, delivering an average 15% performance increase across a benchmark suite of approximately 4,600 tests -- with 23% of those tests seeing improvements of 20% or more. PGO works in combination with tiered compilation by instrumenting tier-0 code and recompiling hot paths with richer profiling data.
In practice, this means long-running services and high-throughput APIs benefit automatically without opting in to any
runtime configuration flags. You can remove any existing DOTNET_TieredPGO=1 environment variable overrides
from your deployment configs, as that setting is no longer necessary.
Other notable codegen improvements include:
- Arm64-specific optimizations including consecutive register allocation for table vector lookup instructions
- AVX-512 ISA support via the new
Vector512<T>type and hardware intrinsics - SIMD-accelerated memory operations (compare, copy, zero) when sizes are known at compile time
- Physical struct promotion (scalar replacement of aggregates) -- previously limited to 4-field structs with primitive fields; now unrestricted
For cloud-native workloads, the new GC.RefreshMemoryLimit() API allows containers to dynamically resize
their memory cap at runtime. This is critical for services that scale down in response to reduced load -- previously,
the GC was unaware of shrinking container limits and could allocate past the new boundary.
// Dynamically lower the GC heap hard limit to 100 MiB and apply it
AppContext.SetData("GCHeapHardLimit", (ulong)100 * 1_024 * 1_024);
GC.RefreshMemoryLimit();
Watch out for aggressive scale-downs: if the new limit leaves insufficient room for the GC to operate, the API returns
a non-zero status. In that case, call GC.Collect(2, GCCollectionMode.Aggressive) first to release as much
memory as possible before retrying.
How does Native AOT work in .NET 8 and when should you use it?
Native AOT (Ahead-of-Time compilation) in .NET 8 compiles your application directly to a native executable at publish time, eliminating the JIT and much of the runtime overhead. This results in faster startup times, lower memory footprint, and no runtime JIT dependency -- making it ideal for containerized microservices, CLI tools, and performance-critical workloads.
The default console app template now includes AOT support out of the box. Scaffolding an AOT-ready project is a single command:
dotnet new console --aot
This template activates three things automatically: publishing to a native self-contained executable via dotnet publish,
enabling trimming/AOT/single-file compatibility analyzers, and enabling debug-time AOT emulation so that problems
(such as unsupported System.Reflection.Emit usage) surface during development rather than at publish time.
Two source generators significantly improve the AOT story in .NET 8. The configuration-binding source generator replaces
reflection-based calls to Bind, Configure<T>, and Get<T> with
statically generated code, and is enabled automatically for AOT-compiled web apps. The System.Text.Json source generator
now reaches feature parity with the reflection serializer, supporting required and init
properties, non-public members, enum-as-string via the new generic JsonStringEnumConverter<TEnum>,
and chained context resolution via TypeInfoResolverChain.
This matters if you are building ASP.NET Core minimal API services or gRPC endpoints that need sub-100ms startup. Most teams will still use the standard JIT for general-purpose web apps, but the AOT path is now production-grade for the right workloads.
What are the most impactful System.Text.Json changes in .NET 8?
System.Text.Json received its most substantial set of improvements since it was introduced, closing a number of long-standing gaps that previously forced teams to reach for Newtonsoft.Json or write custom converters.
Key changes that affect day-to-day usage:
-
Read-only property population: Deserializing into properties that have no
setaccessor now works by opting in with[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]or globally viaPreferredObjectCreationHandling. This eliminates the need for custom converters on many immutable or record-style types. -
snake_case and kebab-case naming policies:
JsonNamingPolicy.SnakeCaseLower,SnakeCaseUpper,KebabCaseLower, andKebabCaseUpperare now built in, removing the need for custom naming policy implementations that most teams were copying from Stack Overflow. - Interface hierarchy serialization: Properties defined on base interfaces are now included in serialization when serializing via a derived interface type.
-
Non-public member support:
[JsonInclude]and[JsonConstructor]can now be applied to internal or private members, enabling cleaner encapsulation without sacrificing serializability. -
Streaming deserialization: New
GetFromJsonAsAsyncEnumerable<T>extension methods onHttpClientallow true streaming consumption of large JSON payloads without buffering the entire response. -
New JsonNode APIs:
DeepClone(),DeepEquals(),ReplaceWith<T>(), and asyncParseAsync()round out the DOM manipulation story.
// Streaming deserialization -- no full-buffer allocation
IAsyncEnumerable<Order?> orders =
httpClient.GetFromJsonAsAsyncEnumerable<Order>("https://api.example.com/orders");
await foreach (Order? order in orders)
{
Console.WriteLine(order?.Id);
}
For AOT or trimming scenarios, you can now also set JsonSerializerIsReflectionEnabledByDefault=false
in your project file to catch accidental reflection usage at build time rather than at runtime.
What SDK and tooling changes in .NET 8 affect build pipelines and developer workflows?
.NET 8 introduces several SDK changes that can silently break existing CI pipelines if you upgrade without reviewing your
build scripts. The most impactful is that dotnet publish and dotnet pack now default to the
Release configuration -- previously they defaulted to Debug. If your pipeline was relying on
the implicit Debug output for non-production use, add -p:PublishRelease=false to preserve the old behavior.
Other SDK changes worth knowing:
-
Terminal Logger:
dotnet build --tlactivates a modernized output mode that groups errors by project, distinguishes multi-targeted builds more clearly, and displays real-time build progress. This is opt-in but worth enabling for interactive developer machines. - Simplified output paths: A new artifacts output layout option consolidates all build outputs (binaries, publish output, NuGet packages) into a single structured directory. This simplifies tooling integration and artifact collection in pipelines.
-
dotnet workload clean: A new command to safely remove orphaned workload packs left behind by
SDK or Visual Studio updates. Run
dotnet workload cleanwhen workload management starts behaving inconsistently after upgrades. - dotnet restore security auditing: Opt in to vulnerability scanning during package restore. Warnings NU1901--NU1904 are emitted for packages with known CVEs. Useful to wire into PR checks.
-
CLI-based MSBuild property evaluation: New
--getProperty,--getItem, and--getTargetResultflags on CLI commands make it straightforward to extract MSBuild values into CI scripts without invoking MSBuild directly or parsing output XML. -
Source Link bundled in SDK: Source Link is now included by default. All builds automatically
embed source commit information in
InformationalVersion, including for libraries targeting earlier .NET versions. This is mostly positive, but watch for cases where you parse that field programmatically.
# Extract a build property in CI without parsing MSBuild XML
dotnet publish --getProperty:OutputPath
# Revert publish to Debug config
dotnet publish -p:PublishRelease=false
What new library APIs in .NET 8 improve testability and correctness in production code?
Several new core library additions in .NET 8 address real gaps in testability, correctness, and performance that teams have historically patched with third-party packages or workarounds.
The TimeProvider abstract class and the ITimer interface introduce a first-class time
abstraction into the BCL. Any code that currently calls DateTime.UtcNow, Task.Delay, or
creates a Timer directly is difficult to unit test. By accepting a TimeProvider dependency,
services can be tested against a fake that controls time progression without thread sleeps or process-level hacks.
The system implementation is available as TimeProvider.System.
// In production
services.AddSingleton<TimeProvider>(TimeProvider.System);
// In tests -- inject a FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing)
var fakeTime = new FakeTimeProvider();
fakeTime.Advance(TimeSpan.FromSeconds(30));
Two new immutable collection types -- FrozenDictionary<TKey, TValue> and FrozenSet<T>
-- are optimized for read-heavy scenarios where the data is populated once at startup and then read frequently.
They trade a slightly higher construction cost for faster lookup operations compared to standard
Dictionary<TKey, TValue>. Most teams will find these useful for configuration lookups,
feature flags, and static data tables.
SearchValues<T> precomputes all data needed to accelerate span searches (such as
IndexOfAny) at construction time, making repeated searches across a fixed set of characters or
bytes significantly faster than passing ad-hoc spans or arrays. This is particularly valuable in parsers,
tokenizers, and HTTP processing code.
The new Random.GetItems<T>() and Random.Shuffle<T>() methods formalize
two operations that most teams previously implemented manually and inconsistently. GetItems selects
a specified number of random elements from a source set (with replacement), while Shuffle randomizes
a span in place using a Fisher-Yates-style algorithm via Random.Shared.
Frequently Asked Questions about .NET 8
Is .NET 8 a Long-Term Support release?
Yes, .NET 8 is an LTS release with three years of support, making it the recommended version for production applications that require a stable, long-lived platform.
Does upgrading to .NET 8 break existing CI pipelines that use dotnet publish?
Potentially yes -- dotnet publish and dotnet pack now default to Release configuration instead of Debug, which is a breaking change if your pipeline relied on the previous default. You can restore the old behavior by passing -p:PublishRelease=false to your publish command.
Do I need to change my System.Text.Json code to take advantage of the new features in .NET 8?
Most improvements are additive and require opting in via attributes or options -- for example, use JsonObjectCreationHandling.Populate for read-only property deserialization, or set PropertyNamingPolicy to JsonNamingPolicy.SnakeCaseLower for automatic snake_case conversion. Existing serialization code will continue to work without modification.
Can I use Native AOT with ASP.NET Core in .NET 8?
Yes, ASP.NET Core minimal API applications support Native AOT publishing in .NET 8. The configuration-binding source generator and the System.Text.Json source generator are both enabled automatically for AOT-compiled web apps, replacing reflection-based implementations without requiring source changes.
What is the TimeProvider class and why should I use it instead of DateTime.UtcNow?
TimeProvider is a new abstract class in .NET 8 that wraps time retrieval and timer creation behind an interface boundary, which allows tests to inject a fake implementation and advance time deterministically without relying on thread sleeps or process-level clock manipulation. In tests, use Microsoft.Extensions.TimeProvider.Testing to get a FakeTimeProvider that you can advance manually.
Does .NET 8 improve performance for applications running on Arm64?
Yes, .NET 8 includes Arm64-specific JIT improvements including consecutive register allocation for table vector lookup instructions and SIMD auto-vectorization of memory operations when sizes are known at compile time, alongside the globally enabled dynamic PGO that benefits all platforms.