
Posted on • Originally published atantondevtips.com on
10 Reasons to Upgrade to .NET 9
.NET 9 and C# 13 were released on November 12, 2024.
In this blog post, I want to show 10 reasons why you should consider upgrading your projects to .NET 9.
On my website:antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.
1. Performance improvements of Minimal APIs
Minimal APIs in .NET 9 received a huge performance boost and can process15% more requests per second than in .NET 8.
Also, Minimal APIs consume93% less memory compared to a previous version.
This means that Minimal APIs became much faster than Controllers.
Minimal APIs have the following advantages over Controllers:
- Better Performance (as I showed you above)
- Single Responsibility: Each endpoint is responsible for one thing, has its own dependencies, compared to bloated Controllers in many applications that have a lot of dependencies that are used by some methods.
- Flexibility and Control: The Minimal API approach allows for more granular control over routing and middleware configuration.
- Development Speed: The ability to quickly set up an endpoint without the ceremony of defining controllers makes you more productive.
- Minimal APIs also align with such modern Concepts as Vertical Slice Architecture.
2. Performance improvements of Exceptions
Exceptions became 2-4 times faster, according to Microsoft Benchmarks.
Exceptions happen everywhere:
- database access error
- external system is down
- file is not found
- request timeout...
Doesn't matter if you use exceptions for control flow or you have Global Exception Handler - good news, exceptions are now faster.
I have always appreciated such performance improvements for free without a need to touch my code.
Remember, exceptions are for exceptional situations. They are not the best option for a control flow.
A better option is aResult Pattern.
3. New HybridCache
When working with cache, you must be aware of the cache stampede problem: when multiple requests receive a cache miss and will all call the database to retrieve the entity.
Instead of calling the database once to get the entity and write it into cache.
Standard MicrosoftIMemoryCache
suffers from this problem.
You can introduce manual locking to solve this problem, but this approach doesn't scale so well.
That's why I am using a new caching libraryHybridCache
available in .NET 9 that solves a cache stampede problem.
HybridCache
is a great replacement of oldIMemoryCache
andIDistributedCache
as it combines them both and can work with:
- in-memory cache
- distributed cache like Redis
To get started withHybridCache
add the following Nuget package:
dotnet add package Microsoft.Extensions.Caching.Hybrid
Here is how you can register it in DI:
#pragma warning disable EXTEXP0018services.AddHybridCache(options=>{options.DefaultEntryOptions=newHybridCacheEntryOptions{Expiration=TimeSpan.FromMinutes(10),LocalCacheExpiration=TimeSpan.FromMinutes(10)};});#pragma warning restore EXTEXP0018
You need to disable the warning with pragma asHybridCache
is in preview at the moment (it might be already released when you're reading this).
By default, it enables the InMemory Cache.
HybridCache
API is similar to those from the old caches.
You can useGetOrCreateAsync
method that will check if entity exists in the cache and returns it.
If the entity is not found - a delegate method is called and to get the entity from the database:
privatestaticasyncTask<IResult>Handle([FromRoute]Guidid,HybridCachecache,ApplicationDbContextcontext,CancellationTokencancellationToken){varbookResponse=awaitcache.GetOrCreateAsync($"book-{id}",asynctoken=>{varentity=awaitcontext.Books.AsNoTracking().Include(b=>b.Author).FirstOrDefaultAsync(b=>b.Id==id,token);returnentityisnotnull?newBookResponse(entity.Id,entity.Title,entity.Year,entity.Author.Name):null;},cancellationToken:cancellationToken);returnbookResponseisnotnull?Results.Ok(bookResponse):Results.NotFound();}
4. New LINQ methods: AggregateBy, CountBy, Index
.NET 9 introduces 3 new LINQ methods:CountBy
,AggregateBy
andIndex
.
Let's explore the code we needed to write before and what improvements these methods bring to the table.
CountBy
Previously, we had to group the items and then count them:
varcountByName=orders.GroupBy(p=>p.Name).ToDictionary(g=>g.Key,g=>g.Count());
TheCountBy
method simplifies this by directly providing a count for each key:
varcountByName=orders.CountBy(p=>p.Name);foreach(varitemincountByName){Console.WriteLine($"Name:{item.Key}, Count:{item.Value}");}
AggregateBy
In older versions, we usedGroupBy
to group orders by category and thenAggregate
within each group to sum up the prices:
vartotalPricesByCategory=orders.GroupBy(x=>x.Category).ToDictionary(g=>g.Key,g=>g.Sum(x=>x.Quantity*x.Price));
The newAggregateBy
method simplifies this process by combining grouping and aggregation into one step:
vartotalPricesByCategory=_orders.AggregateBy(x=>x.Category,_=>0.0m,(total,order)=>total+order.Quantity*order.Price);foreach(varitemintotalPricesByCategory){Console.WriteLine($"Category:{item.Key}, Total Price:{item.Value}");}
Index
In older versions, we manually managed the index or usedSelect
with a projection:
intindex=0;foreach(variteminorders){Console.WriteLine($"Order #{index}:{item}");index++;}foreach(variteminorders.Select((order,index)=>new{order,index})){Console.WriteLine($"Order #{index}:{item}");}
The newIndex
method provides a more elegant solution by providing direct access to the item and its index in a form of Tuple:
foreach(var(index,item)inorders.Index()){Console.WriteLine($"Order #{index}:{item}");}
5. EF Core New Seeding methods.
To seed your database with initial data in previous EF Core versions you had to:
- create seeding with limited capabilities in DbContext in the
OnConfiguring
method - create separate classes, resolve and call them before the application was started (in Program.cs or a Hosted Service).
EF 9 brings a new way to seed your database.
Microsoft recommends usingUseSeeding
andUseAsyncSeeding
methods for seeding the database with initial data when working with EF Core.
You can useUseSeeding
andUseAsyncSeeding
methods directly in theOnConfiguring
method in your DbContext when registering DbContext in DI:
builder.Services.AddDbContext<ApplicationDbContext>((provider,options)=>{options.UseNpgsql(connectionString).UseAsyncSeeding(async(dbContext,_,cancellationToken)=>{varauthors=GetAuthors(3);varbooks=GetBooks(20,authors);awaitdbContext.Set<Author>().AddRangeAsync(authors);awaitdbContext.Set<Book>().AddRangeAsync(books);awaitdbContext.SaveChangesAsync();});});
UseSeeding
is called from theEnsureCreated
method, andUseAsyncSeeding
is called from theEnsureCreatedAsync
method.
When using this feature, it is recommended to implement bothUseSeeding
andUseAsyncSeeding
methods using similar logic, even if the code using EF is asynchronous.
EF Core tooling currently relies on the synchronous version of the method and will not seed the database correctly if theUseSeeding
method is not implemented.
6. EF Core Better LINQ and SQL Translation
EF 9 allows more LINQ queries to be translated to SQL, and many SQL translations for existing scenarios have been improved.
EF 9 has better performance and readability than the previous version.
Let's explore 2 most popular use cases for complex types.
EF9 supports grouping by a complex type instance.
vargroupedAddresses=awaitcontext.Customer.GroupBy(b=>b.Address).Select(g=>new{g.Key,Count=g.Count()}).ToListAsync();
ExecuteUpdate has been improved to accept complex type properties.
However, each member of the complex type must be specified explicitly:
varaddress=newAddress("New York City","Baker's st.","54");awaitcontext.Customers.Where(e=>e.Address.City=="New York City").ExecuteUpdateAsync(s=>s.SetProperty(b=>b.StoreAddress,address));
7. C# 13 Params Collection
Previously, when usingparams
keyword you were able to use only arrays:
PrintNumbers(1,2,3,4,5);publicvoidPrintNumbers(paramsint[]numbers){// ...}
C# 13 introduces Params Collections, allowing you to use the following concrete types:
- Arrays
- IEnumerable
- List
- Span
For example, you can use a list:
List<int>numbers=[1,2,3,4,5];PrintNumbers(numbers);publicvoidPrintNumbers(paramsList<int>numbers){// ...}
And further inPrintNumbers
method you can use, for example, LINQ methods over the params collection.
8. C# 13 New Lock Object
System.Threading.Lock
is a new thread synchronization type in .NET 9 runtime.
It offers better and faster thread synchronization through the API.
How it works:
- Lock.EnterScope() method enters an exclusive scope and returns a ref struct.
- Dispose method exits the exclusive scope
When you replaceobject
with a newLock
type inside alock
statement, it starts using a new thread synchronization API.
Rather than using old API throughSystem.Threading.Monitor
.
publicclassLockClass{privatereadonlySystem.Threading.Lock_lockObj=new();publicvoidDo(inti){lock(_lockObj){Console.WriteLine($"Do work:{i}");}}}
9. ASP.NET Core: Static Asset delivery optimization
Did you know that you can serve static files from your ASP NET Core application?
Kestrel web server supports static files, and in .NET 9 it became even faster.
You can host your backend and frontend code using a single service (executable), so you don't need an extra service or docker container to host your frontend inside a nginx or Apache.
In .NET 8 and earlier versions you could add static files usingUseStaticFiles
function.
In .NET 9 there is a new function calledMapStaticAssets
which is a preferable way of serving static content.
In .NET 9 static files are much better compressed compared to .NET 8.
MapStaticAssets
provides the following benefits when compared toUseStaticFiles
:
- 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 content has changed.
10. Blazor Improvements
.NET 9 brings the following nice updates to Blazor:
- .NET MAUI Blazor Hybrid and Web App solution template
- Detect rendering location, interactivity, and assigned render mode at runtime
- Improved server-side reconnection experience
- Simplified authentication state serialization for Blazor Web Apps
- Add static server-side rendering (SSR) pages to a globally-interactive Blazor Web App
- Constructor injection
- Websocket compression for Interactive Server components
- Handle keyboard composition events in Blazor
Summary
.NET 9 release has brought a lot of new and improvements to the existing features.
The main focus for this release was:
- to improve the performance of .NET runtime, including WebApplications running on Kestrel, Minimal APIs, Exceptions, EF Core, LINQ and others
- to extend existing features to support more scenarios
- add completely new features to make development better and more enjoyable
Note: .NET 9 is STS (Standard Term Support) but Microsoft's stated quality of STS and LTS (Long-Term Support) are the same.
So I see no reason why you shouldn't upgrade to .NET 9.
I have already migrated a lot of services to .NET 9 without a significant effort.
On my website:antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.
Top comments(3)

- Email
- LocationAthens
- EducationBSc University of Sunderland- MSc University of Greenwich
- PronounsTech Lead and Senior Software Engineer with a strong focus on .NET technologies.
- WorkTech Lead | Senior Software Engineer |
- Joined
Thank you for sharing these valuable tips , they're helpful!

- LocationUkraine
- WorkMicrosoft MVP | Technical Lead
- Joined
You're welcome!

HybridCache is both an abstraction and a Microsoft-provided default implementation.
The abstraction came out with .NET 9 and is GA, whereas the Microsoft-provided implementation is not out yet (it's in preview).
But something else you can try right now is FusionCache: with the v2 release a couple of weeks ago it is now also the first production-ready implementation of HybridCache (but with more features 😬).
Hope this helps.
For further actions, you may consider blocking this person and/orreporting abuse