Upgrading to .NET 8: Part 5 - Preview 7 and Release Candidates 1 and 2

Highlights from upgrading to .NET 8 preview 7 and release candidates 1 and 2

13 October 2023 by Martin Costello |
13 October 2023 by Martin Costello

This post is a bumper edition, covering three different releases:

I had intended to continue the post-per-preview series originally, but time got away from me with preview 7, plus there wasn't much to say about it, and then I went on holiday for two weeks just as release candidate 1 landed. Given release candidate 2 was released just a few days ago, instead I figured I'd just catch-up with myself and summarise everything in this one blog post instead!

Release Candidate 2 is also the last planned release before the final release of .NET 8 in November to coincide with .NET Conf 2023, so this is going to be the penultimate post in this series.

Preview 7

From upgrading the various projects I've been testing with .NET 8, there were no new issues to report with preview 7 in August. There were, however, a few changes that I needed to make with the introduction of a number of new warnings from analyzers added to the .NET SDK and some of the new libraries.

More CA1849 Warnings

A number of improvements were made to the CA1849 analyzer which resulted in a number of new warnings being identified in code where synchronous versions of APIs were still being used despite asynchronous versions now being being available.

The main one flushed out in this preview for the code I've been testing was this:

using var stream = await response.Content.ReadAsStreamAsync(Context.RequestAborted);
using var document = JsonDocument.Parse(stream);

In these cases, the fix is trivial:

using var stream = await response.Content.ReadAsStreamAsync(Context.RequestAborted);
using var document = await JsonDocument.ParseAsync(stream);

FakeTimeProvider Experimental Warnings

Usage of the new FakeTimeProvider class started to emit a EXTEXP0004 warning which needed to be suppressed. These warnings were removed for RC2 by dotnet/extensions#4455 after the new libraries being formally API reviewed, but these just seemed to add noise for people so I'm not sure why they were needed in the first place if the intention was to ship the APIs as stable for .NET 8 itself.

Typically things like [RequiresPreviewFeatures(...)] are used in the core libraries for such warnings, and then only when a non-stable API is intended to be shipped in a stable release, such as with the Generic Math APIs in .NET 6.

If this pattern were replicated for all new APIs added in new .NET releases before they reach release candidate it would create a lot of noise for early adopters in my opinion. I hope such warning patterns don't become commonplace across .NET in the future. 😄

CompositeFormat

As part of the performance improvements in .NET 8, a new type, CompositeFormat, has been added to move the logic for formatting strings with composite formatting out of string.Format() every time it is used for a specific usage to instead be amortised across all usages with one upfront cost of parsing it. More information about this can be found in Stephen Toub's epic blog post about performance in .NET 8 (grab a coffee first ☕).

The TL;DR for this though is to identify any usages of string.Format() that can be improved this way, so a new CA1863 analyzer warning has been added to the .NET SDK.

An example of a Git diff to change some string formatting to use this new pattern is shown below.

using System;
using System.Globalization;
+ using System.Text;

public class Greeter
{
+   private static readonly CompositeFormat GreetingFormat = CompositeFormat.Parse("Hello {0}!");
    public void PrintName(string name)
    {
-       Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Hello {0}!", name));
+       Console.WriteLine(string.Format(CultureInfo.InvariantCulture, GreetingFormat, name));
    }
}

Release Candidate 1

Release candidate 1 was released in September, and it was also the first release of .NET 8 that has a Go-Live license, meaning it is supported by Microsoft for use in production. Once I got back from my holiday, I updated all of the production (and "production") applications I'm responsible for to run on .NET 8 using RC1.

Everything worked as expected once deployed, but there were a number of build-time issues that needed resolving.

Configuration Binding Source Generator

There were a number of regressions in changes made to the new Configuration Binding Source Generator which unfortunately meant that it would not produce code that would compile for a number of my ASP.NET Core applications.

This meant that I had to turn it off completely for RC1. The issues below were all fixed for RC2, so I was able to re-enable it then.

Daily Build Testing

Following the testing with .NET 8 daily builds I did to check a fix for the new Request Delegate Generator in preview 6, I looked into updating my update-dotnet-sdk GitHub Action to support updating repositories based on the output of the dotnet/installer repository. With the v2.3.0 release of the action, that became supported. With that released I started to set up parallel branches in some of my repositories to test the daily builds of .NET 8 where I felt they'd use a wide range of capabilities to give good test coverage.

I might write a more in-depth blog post about this at some point as I'm planning on using this approach again next year for .NET 9 starting around preview 1. It's found a number of bugs that got caught before an official preview release was published, so I think it's been a valuable investment of time.

Such an issue was dotnet/runtime#9038. This was an issue where changes to HttpClientFactory caused a background task to throw an exception when it was disposed. This in turn caused the .NET test process to "crash", causing all of my CI builds to fail. I was glad this was caught before RC1 as it would have been a big blocker for me personally, as it would have caused a lot of test failures in my repositories that use the HttpClientFactory to stub out HTTP requests in the integration tests (Reliably Testing HTTP Integrations in a .NET Application).

Portable Runtime Identifiers

In .NET 8 the Runtime Identifiers (RIDs) used by projects targeting .NET 8 have been changed to be shorter/simpler and more portable (see dotnet/docs#36527).

This had the consequence that for applications being published as a self-contained deployment for their targeted operating system and architecture, the RID being previously used would now generate a build error.

For example, instead of publishing for win10-x64 the RID is now win-x64.

This was a simple enough change to make, but affected a fair number of my repositories adopting .NET 8 as a result.

C# 12 by Default

The RC1 release also changed the default language version for C# projects to be C# 12.

Previously you needed to explictly set the LangVersion MSBuild property to either preview or 12, but as-of RC1 the value of latest (which is what I use) was updated to point to C# 12.

This change in turn caused a number of new warnings to be emitted by the compiler to suggest that new code patterns be used where beneficial. A major source of these warnings came from the new collection expressions language feature.

For example, instead of writing this: List<string> names = new() { "Alice", "Bob", "Charlie" };

You can now write this: List<string> names = ["Alice", "Bob", "Charlie"];

Much terser!

From using it in a few places as suggested, I think I quite like the syntax as it is similar to JavaScript/TypeScript array usage, so it feels quite natural to use to me. It also has the benefit of being able to leverage some new compiler smarts to help improve the performance and memory allocation of your applications.

There were however two cases where analysers didn't like particular code constructs for C# 12. These needed to be suppressed, but hopefully they'll be resolved in future releases of the relevant projects:

New Analyzers

There were also a few new analyzers added to the .NET SDK in RC1 that raised new warnings.

CA1869

The new CA1869 analyzer warns if you are repeatedly creating a new JsonSerializerOptions instance in your code.

Doing this can potentially be a huge pit of failure in an application.

I made a mistake in a production application during the .NET 5 timeframe where a misconfiguration of the dependency injection container for an application meant that a new instance of JsonSerializerOptions was being created for every HTTP request, rather than just once at startup. In that instance the application in question was affected to the tune of a 200% increase in response times for requests!

Spot where the code change got deployed... 🕵️

A graph showing the performance impact of the bad change

While in this case the analyzer may not have helped as it was an issue with the dependency injection configuration, it's good to know that investment has been made to help avoid developers causing the same problem in different scenarios.

NETSDK1212

The trim analyzer for AOT has been present in the .NET SDK for a while, but what I didn't realise is that it isn't supported for use on projects that don't target at least .NET 6. A new analyzer warning, NETSDK1212, was added to warn about this. This is easy to fix if you have a project doing multi-targeting (such as Polly) by adding a condition to project files where the analyzer is enabled, like so:

<PropertyGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">
  <EnableAotAnalyzer>true</EnableAotAnalyzer>
  <EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
  <EnableTrimAnalyzer>true/EnableTrimAnalyzer>
  <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

Release Candidate 2

Release candidate 2 was released in October just a few days ago, and it's been pretty uneventful (for me at least).

Other than one issue I ran into when I re-enabled the Configuration Binding Source Generator for one project now the other issues I mentioned above had been fixed, I didn't experience any friction with RC2 at all.

Within a few hours of RC2 being released, I had updated all of the repositories I'm responsible for that were running RC1 to use .NET 8 RC2 instead. 🚀😎

Using my GitHub Action, in most cases I didn't even make the changes myself!

For example, this pull request was automatically created to update one of my apps from RC1 to RC2, and the deployed application produced in that repository approved it and deployed it to production within 75 minutes of RC2 being released with no manual intervention at all. Most of the delay was waiting for GitHub Actions runners to be available due to the contention caused by trying to update all my repositories at the same time!

Summary

With those repositories updated, it's just a case of waiting for the final release of .NET 8 in November and then merging all of the other updates to things like libraries I've been waiting for the stable release to merge.

For example, updating Polly to use the new TimeProvider API and replace the internal copy of the code used for the v8.0.0 release.

.NET 8 is shaping up to be a great release that will make your applications faster than ever with minimal developer effort to upgrade and reap the benefits, even if you don't use any new capabilities.

In the next and final post in this series, we'll take a look at completing the upgrades to the stable .NET 8 release!

Upgrading to .NET 8 Series Links

You can find links to the other posts in this series below.