Blogs

Unveiling .NET 8 – Part 1: New Features and Capabilities

Introduction

The release of .NET 8 has been anticipated by many .NET and Azure developers for a while now, as it marks the latest long-term support (LTS) release since .NET 6. Due to this fact, many organizations have ignored the release of .NET 7, and as such this latest release compounds a lot of the excitement and functionality missed out by the .NET 7 release. Furthermore, the release of .NET 8 brings with it several other exciting announcements, such as the achievement of isolated Azure Functions reaching feature parity with the in-process model, which we will explore later in this two-part blog series.

Are you excited to make the move to .NET 8 to enjoy the new features and language updates? Or, are you excited by the prospect of feature parity that isolated function apps will finally have with the in-process model? Whether you’re eager to explore the innovations introduced in .NET 8 and C# 12 or looking forward to the newfound feature alignment in isolated function apps, this two-part series offers a comprehensive dive into the world of .NET 8, as well as its impact on Azure Functions.

In this article, we’ll cover the biggest changes coming to .NET 8, in terms of performance, features, standard library changes, syntax changes, and so on!

Overview

In the first part of this two-part series, we’ll cover some of the changes that .NET 8 brings to the table, from performance upgrades to language changes and everything in between. For a comprehensive view of everything else that .NET 8 brings to the table, see more here!

At a high level, this update brings the following changes and features to .NET:

  • Serialization improvements
  • A time abstraction
  • UTF8 improvements
  • New methods for working with randomness
  • New performance-focused types
  • System.Numerics and System.Runtime.Intrinsics
  • New data validation attributes
  • New metrics APIs
  • Extended cryptography support
  • Support for HTTPS proxy
  • New stream-based ZipFile methods

This update also brings C# 12 along with it, which brings forth the following features:

  • Primary constructors
  • Collection expressions
  • ref readonly parameters
  • Default lambda parameters
  • Alias any type
  • Inline arrays
  • Experimental attributes
  • Interceptors (Preview only)

Examples

There’s a lot to unpack in this update, with a lot of changes that are useful in many niche circumstances: As such, we will explore some examples of the more commonly used features that .NET 8 brings to the table.

Performance

The traditional performance update summary that comes with each .NET release, aptly named “Performance Improvements in .NET 8” this year, is a compilation of hundreds of pages worth of performance changes, ranging from slight to great. Due to the sheer size, we can’t cover everything, but there are some interesting things to note:

  • The underlying Enum class has been rewritten to provide increased performance to methods such as IsDefined, GetValues, GetNames, EnumToString etc.
  • Performance improvements to AddRange for Lists, whilst also adding Span-related variants to AddRange, InsertRange and CopyTo.
  • ToString has been made faster for ints by caching digits 0 to 299 (a somewhat arbitrary sounding choice, though it does include all the HTTP success codes). This also reduces allocations somewhat.
  • Dynamic Profile-Guided Optimization, or PGO, has been enabled by default, a change so important that Microsoft partner Stephen Toub who wrote the article on performance improvements touts it as “a one-character PR that might be the most valuable PR in all of .NET 8”. At a high level, it is an optimization technique that is combined with tiering to improve the performance of code by profiling its actual runtime behavior and taking the profiled data to perform smart optimizations.

The rest of the changes are across a large variety of areas, such as the JIT compiler, the GC and VM, threading, reflection, other primitives, I/O, networking, and even more!

Native Ahead of Time Compilation

Native AoT was shipped with .NET 7, however, .NET 8 takes this feature and expands on it greatly. Compiling a .NET application with AoT enabled creates a self-contained executable or library made entirely from native code, requiring no JIT (Just in Time) compilation at runtime. In fact, no JIT-related code is even shipped to the resulting executable or library. As an AoT-compiled application requires no JIT compilation, the issue of cold starts is almost mitigated completely.

To enable this, one can add an additional flag to the appropriate .csproj file:

<PublishAot>true</PublishAot>

Microsoft has presented a comparison between an AoT-compiled app produced in .NET 7 vs .NET 8 to showcase the improvements in file size to the compilation mode:

# .NET 7
# 12.8 MB
dotnet publish -f net7.0 -r linux-x64 -c Release
ls -s --block-size=k
> 12820K /home/stoub/nativeaotexample/bin/Release/net7.0/linux-x64/publish/nativeaotexample

# .NET 8
# 1.5 MB
dotnet publish -f net8.0 -r linux-x64 -c Release
ls -s --block-size=k
> 1536K /home/stoub/nativeaotexample/bin/Release/net8.0/linux-x64/publish/nativeaotexample

Alongside these clear improvements in output size, there have been a lot of performance improvements to parts of the AoT compiler that lack some optimizations that the JIT compiler offers. Most important, however, is the support of ASP.NET web apps. This support is almost fully featured and allows many web apps to make the switch to native AoT. With that said, there are some changes that need to be made to the application code to support this, such as opting into source generators for JSON serialization and so forth.

Primary Constructors

Primary constructors are a feature that, for a while, only `record` style objects have had to enjoy. In .NET 8 however, the syntax is being made available for standard `class` objects also. This provides us a way to clean up a lot of boilerplate code in classes where oftentimes the parameters in the constructor are directly equivalent to backing properties or fields. This means that not only can a lot of POCOs be neatened up, but also classes that are heavy with constructor-based dependency injection. For example:

// Old
public class HttpTrigger
{
    private readonly ILogger<HttpTrigger> _logger;
    private readonly IPopularBooksService _popularBooksService;

    public HttpTrigger(ILogger<HttpTrigger> logger, IPopularBooksService popularBooksService)
    {
        _logger = logger;
        _popularBooksService = popularBooksService;
    }
}

// New
public class HttpTrigger(ILogger<HttpTrigger> Logger, IPopularBooksService PopularBooksService)
{
 
}

Collection Expressions

Collection expressions are another addition in C# 12 that provides syntactic sugar to developers, and they do so by providing terse shorthands for creating and destructuring collections. Using square brackets, `[]`, we can signify the creation of new arrays, lists, and spans, whilst by using `..`, we can signify that the contents of a given collection are to be expanded in place. These provide a convenient syntax to reduce potential boilerplate code. For example:

// Old
string[] updatedReviews = existingBook.Reviews.Concat(new[] { evt.Review }).ToArray();

// New
string[] updatedReviews = [..existingBook.Reviews, evt.Review];

Time Provider

For the longest time, testing functions that were centered around or consumed pieces of time via the DateTime or DateTimeOffset APIs has been a hassle. .NET 8 brings with it an abstraction known as TimeProvider, an abstract class that can be very simply extended and consumed in services where we might need to use the current time at execution.

By depending on the `TimeProvider` abstract class, we can inject our actual implementation when dealing with production code. A contrived example of an implementation might look like the following service:

public class StandardTimeProvider : TimeProvider
{
    public override DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;
}

When we go to test the consuming service, we can substitute in a testable variant via a new NuGet package that Microsoft exposes, Microsoft.Extensions.TimeProvider.Testing. With this package, we can deterministically test any classes consuming the abstract TimeProvider class like so:

namespace Example;

class ServiceThatUsesTime
{
    private readonly TimeProvider _timeProvider;

    public ServiceThatUsesTime(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public bool IsWeekday()
    {
        var now = _timeProvider.UtcNow;
        return now.DayOfWeek != DayOfWeek.Saturday && now.DayOfWeek != DayOfWeek.Sunday;
    }
}

[Test]
public void TimeProviderMockingExample()
{
    var mock = new FakeTimeProvider();

    // Sets the time provider mock to deterministically return a Monday
    mock.SetUtcNow(DateTime.Parse("2023-12-04"));

    var svc = new ServiceThatUsesTime(mock);

    // Consistently returns true
    var result = svc.IsWeekday();

    Assert.IsTrue(result);
}

Performance-Focused Types

Some new performance-focused types have been added to the standard library, such as the frozen collections FrozenSet and FrozenDictionary. These types are immutable upon creation, which allows for certain optimizations to be made to make reads even faster than they would be with the standard `Set` or `Dictionary` types.

private readonly FrozenSet<string> _bookNames = new HashSet<string>() { "The Hobbit", "The Lord of the Rings", "The Silmarillion" }.ToFrozenSet();

_bookNames.Contains(book.Value.Name);

Another performance-focused type is SearchValues, which provides an incredibly performant way of finding a char within a string. This is used extensively internally within the .NET compiler, replacing many such standard IndexOfAny calls, such as in this example for ReplaceLineEndings:

// Old
internal static int IndexOfNewlineChar(ReadOnlySpan<char> text, out int stride)
{
    const string Needles = "\r\n\f\u0085\u2028\u2029";
    int idx = text.IndexOfAny(needles);
    ...
}

// New
internal static class SearchValuesStorage
{
    public static readonly SearchValues<char> NewLineChars = SearchValues.Create("\r\n\f\u0085\u2028\u2029");
}

int idx = text.IndexOfAny(SearchValuesStorage.NewLineChars);

Keyed Dependency Injection

For a while, to inject variations of an interface into the dependency injection container and have different consumers target specific implementations, you’d need to either reach for AutoFac, create a provider class, or pull down a list of all implementations and use some discriminator to find the one you need. This was predominantly an issue when dealing with classes that may be configured in multiple ways depending on the consumer, such as caches. ConsumerA might want an instance of IMemoryCache that has a large size limit and a certain compaction rate, whilst ConsumerB might want an instance that has a small limit.

Now, we can use keyed dependency injection to have consumers target which version of the class or interface they’d like to receive:

builder.Services.AddKeyedSingleton<IMemoryCache, BigCache>("big");
builder.Services.AddKeyedSingleton<IMemoryCache, SmallCache>("small");

public class ServiceA([FromKeyedServices("big")] IMemoryCache bigCache)
{
    public string Get(string id) => bigCache.Get(id);
}

public class ServiceB([FromKeyedServices("small")] IMemoryCache smallCache)
{
    public string Get(string id) => smallCache.Get(id);
}

Up Next

In this part of the two-part series, we investigated a few of the more commonly used features and functionalities unlocked by .NET 8, however, in the next part we’ll set our sights on the latest changes available in Azure Functions!

[mailpoet_form id="1"]

Other Recent Blogs

Microsoft Teams IP Phones and Intune Enrollment

Microsoft Teams provides a growing portfolio of devices that can be used as desk and conference room phones. These IP phones run on Android 8.x or 9.x and are required to be enrolled in Intune. By default, these devices are enrolled as personal devices, which is not ideal as users should not be able to enrol their own personal Android devices.

Read More »

Level 9, 360 Collins Street, 
Melbourne VIC 3000

Level 2, 24 Campbell St,
Sydney NSW 2000

200 Adelaide St,
Brisbane QLD 4000

191 St Georges Terrace
Perth WA 6000

Level 10, 41 Shortland Street
Auckland

Part of

Arinco trades as Arinco (VIC) Pty Ltd and Arinco (NSW) Pty Ltd. © 2023 All Rights Reserved Arinco™ | Privacy Policy | Sustainability and Our Community
Arinco acknowledges the Traditional Owners of the land on which our offices are situated, and pay our respects to their Elders past, present and emerging.

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.