.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 process 15% more requests per second than in .NET 8.
Also, Minimal APIs consume 93% 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 a Result 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 Microsoft IMemoryCache
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 library HybridCache
available in .NET 9 that solves a cache stampede problem.
HybridCache
is a great replacement of old IMemoryCache
and IDistributedCache
as it combines them both and can work with:
- in-memory cache
- distributed cache like Redis
To get started with HybridCache
add the following Nuget package:
dotnet add package Microsoft.Extensions.Caching.Hybrid
Here is how you can register it in DI:
#pragma warning disable EXTEXP0018
services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10),
LocalCacheExpiration = TimeSpan.FromMinutes(10)
};
});
#pragma warning restore EXTEXP0018
You need to disable the warning with pragma as HybridCache
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 use GetOrCreateAsync
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:
private static async Task<IResult> Handle(
[FromRoute] Guid id,
HybridCache cache,
ApplicationDbContext context,
CancellationToken cancellationToken)
{
var bookResponse = await cache.GetOrCreateAsync($"book-{id}",
async token =>
{
var entity = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.FirstOrDefaultAsync(b => b.Id == id, token);
return entity is not null ? new BookResponse(entity.Id, entity.Title, entity.Year, entity.Author.Name) : null;
},
cancellationToken: cancellationToken
);
return bookResponse is not null ? Results.Ok(bookResponse) : Results.NotFound();
}
4. New LINQ methods: AggregateBy, CountBy, Index
.NET 9 introduces 3 new LINQ methods: CountBy
, AggregateBy
and Index
.
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:
var countByName = orders
.GroupBy(p => p.Name)
.ToDictionary(
g => g.Key,
g => g.Count()
);
The CountBy
method simplifies this by directly providing a count for each key:
var countByName = orders.CountBy(p => p.Name);
foreach (var item in countByName)
{
Console.WriteLine($"Name: {item.Key}, Count: {item.Value}");
}
AggregateBy
In older versions, we used GroupBy
to group orders by category and then Aggregate
within each group to sum up the prices:
var totalPricesByCategory = orders
.GroupBy(x => x.Category)
.ToDictionary(
g => g.Key,
g => g.Sum(x => x.Quantity * x.Price)
);
The new AggregateBy
method simplifies this process by combining grouping and aggregation into one step:
var totalPricesByCategory = _orders.AggregateBy(
x => x.Category,
_ => 0.0m,
(total, order) => total + order.Quantity * order.Price
);
foreach (var item in totalPricesByCategory)
{
Console.WriteLine($"Category: {item.Key}, Total Price: {item.Value}");
}
Index
In older versions, we manually managed the index or used Select
with a projection:
int index = 0;
foreach (var item in orders)
{
Console.WriteLine($"Order #{index}: {item}");
index++;
}
foreach (var item in orders.Select((order, index) => new {order, index}))
{
Console.WriteLine($"Order #{index}: {item}");
}
The new Index
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) in orders.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 using UseSeeding
and UseAsyncSeeding
methods for seeding the database with initial data when working with EF Core.
You can use UseSeeding
and UseAsyncSeeding
methods directly in the OnConfiguring
method in your DbContext when registering DbContext in DI:
builder.Services.AddDbContext<ApplicationDbContext>((provider, options) =>
{
options
.UseNpgsql(connectionString)
.UseAsyncSeeding(async (dbContext, _, cancellationToken) =>
{
var authors = GetAuthors(3);
var books = GetBooks(20, authors);
await dbContext.Set<Author>().AddRangeAsync(authors);
await dbContext.Set<Book>().AddRangeAsync(books);
await dbContext.SaveChangesAsync();
});
});
UseSeeding
is called from the EnsureCreated
method, and UseAsyncSeeding
is called from the EnsureCreatedAsync
method.
When using this feature, it is recommended to implement both UseSeeding
and UseAsyncSeeding
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 the UseSeeding
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.
var groupedAddresses = await context.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:
var address = new Address("New York City", "Baker's st.", "54");
await context.Customers
.Where(e => e.Address.City == "New York City")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, address));
7. C# 13 Params Collection
Previously, when using params
keyword you were able to use only arrays:
PrintNumbers(1, 2, 3, 4, 5);
public void PrintNumbers(params int[] 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);
public void PrintNumbers(params List<int> numbers)
{
// ...
}
And further in PrintNumbers
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 replace object
with a new Lock
type inside a lock
statement, it starts using a new thread synchronization API.
Rather than using old API through System.Threading.Monitor
.
public class LockClass
{
private readonly System.Threading.Lock _lockObj = new();
public void Do(int i)
{
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 using UseStaticFiles
function.
In .NET 9 there is a new function called MapStaticAssets
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 to 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 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 (0)