Upgrading to .NET 8: Part 4 - Preview 6
Highlights from upgrading to .NET 8 preview 6
Following on from part 3 of this series, I've been continuing to upgrade my projects to .NET 8 - this time to preview 6. In this post I'll cover more experiences with the new source generators with this preview as well as a new feature of C# 12: primary constructors.
More Fun with Source Generators
Request Delegate Generator - Round 3
Preview 6 includes further changes to the Request Delegate Generator since last month's preview 5, so time to dive in again and see how things are shaping up.
This release included what I thought was the fix to exclude the generated code from the code coverage reports. However, it turns out that I misunderstood the coverlet code coverage defaults, and generated code still isn't excluded by default. It's still desirable that the code that's generated has this attribute, so it's not a wasted effort, but it means that it still requires me to change my coverlet configuration to not include the code in my coverage reports.
This is easily fixed with the following addition to my project files where I use the coverlet MSBuild integration: <ExcludeByAttribute>GeneratedCodeAttribute</ExcludeByAttribute>
.
Once I updated my configuration, I enabled the Request Delegate Generator in all of the repositories I'm testing .NET 8 with. This was mostly successful, but I did find two issues with the generator.
The first was an issue with code being generated that didn't honour nullable reference types correctly for a required struct. As of writing this blog post this is still being looked into.
The second issue was that the generator wasn't emitting code correctly where the lambda method for the endpoint captures a type parameter from the method calling the Map*()
method. For example:
public static RouteHandlerBuilder MapRepositoryUpdate<T>(
this IEndpointRouteBuilder endpoints,
string pattern,
Func<IConfigurationRepository, RepositoryId, T, CancellationToken, Task<bool>> operation)
{
return endpoints.MapPatch(pattern, async (
long installationId,
long repositoryId,
[FromBody] Payload<T> request,
InstallationService service,
IConfigurationRepository repository,
CancellationToken cancellationToken) =>
{
if (!await service.UserHasAccessToRepositoryAsync(installationId, repositoryId))
{
return Results.NotFound();
}
await repository.EnsureRepositoryAsync(repositoryId, cancellationToken);
return await operation(repository, new(repositoryId), request.Value, cancellationToken) switch
{
false => Results.Conflict(),
true => Results.NoContent(),
};
}).RequireAuthorization();
}
internal sealed record Payload<T>(T Value);
This turned out to be a known issue, but unfortunately it doesn't appear to be in scope to be resolved as part of the .NET 8 release. Instead in preview 7 the generator will emit a warning for this scenario (and others) that aren't supported as part of this year's release.
Along with this testing, Safia Abdalla from the ASP.NET Core team reached out to me and asked if I'd do some testing with the latest nightly builds of ASP.NET Core with Request Delegate Generator enabled. This is because quite a big change was made for preview 7 to build the source generators on top of the new C# 12 feature: Interceptors.
I was happy to help, so I updated a few of my projects to use the latest nightly builds from the dotnet/installer repository. I didn't find any issues with the changes which is a great sign for preview 7 on that front, but I did find a new issue with the Just in Time (JIT) compiler related to SIMD12 instructions on Linux x64 that caused my tests to crash. This issue should be fixed as part of preview 7 too.
Configuration Binding Source Generator
From one source generator to another, preview 6 also includes various fixes to the Configuration Binding Source Generator that was introduced in preview 3. I hadn't tried this out yet, so again I turned this on in all of the repositories I'm testing .NET 8 with. This was certainly a fruitful excercise in terms of finding bugs!
Across the various repositories I turned the generator on for, I found a total of four different issues with the code produced by the generator.
- Another variant on the
[GeneratedCode]
attribute missing, impacting code coverage - dotnet/runtime#89007 - A compiler error generating code for a struct - dotnet/runtime#89010
- The generator failing to generate any code on Linux and macOS - dotnet/runtime#89014
- A compiler error generating code for for nullable reference types - dotnet/runtime#89019
As a bonus, while the .NET team investigated the issues I found, they also found a fifth issue where redundant code was being generated.
I think these issues again highlight how valuable community testing of pre-releases of .NET can be. Finding these issues earlier allows for more use cases to be flushed out while features are still under development, leading to the new features being more stable and having more depth of coverage ahead of release candidates and the final release being made available. The sooner issues are identified, the more time the .NET team has to fix them before shipping! 🚢
Primary Constructors
Primary constructors are a new feature in C# 12. In short, they allow you remove the need to define a traditional explicit constructor to pass parameters to use in a class or struct. Instead, you declare the parameters as part of the class/struct declaration, where you can then capture the parameters to use in the members of the type, such as to assign the default value of a property.
To use primary constructors, you just need to opt-in to C#12 by enabling preview language features in the .NET SDK in your project file(s) (or in Directory.Build.props
) like this: <LangVersion>preview</LangVersion>
This isn't a new feature in preview 6, but this is the first preview I've tried it out with. To be honest, I wasn't particularly excited about this feature when it was first announced, but now I've tried it I've been won over. Suprisingly, the place that really won me over was in some of my test code!
For example, to output logs from ASP.NET Core in xunit tests, you need to pass an instance of ITestOutputHelper
to the constructor of your test class. This then lets you redirect the logs from your application under test to xunit using a package such as my xunit logging NuGet package.
In many of my projects, I have a base class for my tests that handles this for me, but it still needs the derived test classes to also declare a constructor to pass through the ITestOutputHelper
instance. This results in a test class that looks something like this:
public class MyTests : TestsBase
{
public MyTests(ITestOutputHelper outputHelper)
: base(outputHelper)
{
}
// Here be tests...
}
With the adoption of primary constructors, this can be simplified to the following:
public class MyTests(ITestOutputHelper outputHelper) : TestsBase(outputHelper)
{
// Here be tests...
}
Code formatting preferences aside, I think this is a great improvement as it not just reduces the number of lines of code you need in your types, but I think it also makes the code a lot neater and has less ceremony.
This is one of those changes where I might however not adopt the change en masse in existing projects to reduce code churn for peers reviewing pull requests, but is the sort of change I'd slowly adopt over time as I'm working on a project asI touch individual files.
Overall I think this is a nice addition to C# as it continues to evolve.
Summary
In this post we looked at the latest changes to the new source generators coming as part of the .NET 8 release, as well as looking at a use case for adopting primary constructors.
In the next post in this series, we'll take a look at upgrading to .NET 8 Preview 7 as well as release candidates 1 and 2: Part 5 - Preview 7 and Release Candidates 1 and 2.
Upgrading to .NET 8 Series Links
You can find links to the other posts in this series below.
- Part 1 - Why Upgrade?
- Part 2 - Automation is our Friend
- Part 3 - Previews 1-5
- Part 4 - Preview 6 (this post)
- Part 5 - Preview 7 and Release Candidates 1 and 2
- Part 6 - The Stable Release