Zoo 105 Podcast: Migration from .NET Core 3.1 to .NET 6

Zoo 105 Podcast: Migration from .NET Core 3.1 to .NET 6

Azure Functions have been updated to support the latest version of .NET, the 6.0, that is LTS (Long Term Support).
I have taken the opportunity to update my project, the Azure Function that generates the podcast for Zoo di 105.
In this blog post I want to describe shortly the improvements brought by .NET 6 and C# 10.

Project files

The project files (for the Azure functions project, plus an import utility) have been modified:

  • upgrading the target framework, from netcoreapp3.1 to net6.0-windows (I have created this GitHub issue asking why I can't simply use net6.0);
  • increasing the Azure Functions Version, from v3 to v4.
  • updating the various NuGet packages to their latest versions.

File scoped namespaces

C# 10 introduces file scoped namespaces.

Essentially instead of having something like:

namespace Zoo105Podcast {
	...
}

Now it can be shortened in this way:

namespace Zoo105Podcast;

...

This saves space both horizontally (the code needs one less indentation) and vertically (no curly brackets required); also the change is very easy to do.

Init-only setters

C# 9 introduces init-only setters.

In short, an init-only property can be written only during initialization (that means: during construction or in an object initializer).
Whenever possible, it's better to use init-only properties, if they don't change after the instance of the class has been created.

Even this change is very easy to do.

For example, from:

public class Podcast2Download
{
	// Id of the podcast, in format zoo_yyyyMMdd or 105polaroyd_yyyyMMdd
	public string Id { get; set; }
	public DateTime DateUtc { get; set; }
	public string FileName { get; set; }
	public Uri CompleteUri { get; set; }
}

to:

public class Podcast2Download
{
	// Id of the podcast, in format zoo_yyyyMMdd or 105polaroyd_yyyyMMdd
	public string Id { get; init; }
	public DateTime DateUtc { get; init; }
	public string FileName { get; init; }
	public Uri CompleteUri { get; init; }
}

Nullable reference types

C# 8 has introduced nullable reference types, but I have waited to adopt them, because of an issue that I will describe later.

This option was not enabled by default in previous versions of .NET, but since .NET 6, it is enabled by default. In any case, I have enabled it explicitly in my Directory.build.props, together with the associated warnings to be treated as errors:

<Project>

	<PropertyGroup>
		<Nullable>enable</Nullable>
		<WarningsAsErrors>CS8600;CS8602;CS8603;CS8618;CS8625</WarningsAsErrors>
	</PropertyGroup>

</Project>

This has indeed confirmed checks that I was doing remembering that for example some cache object could be null, while for example a support method that was returning the cache object, or creating it, was indeed returning a non-null value:

public class CosmosHelper : IDisposable
{
	...
	private Container? cosmosContainerCache;
   
	private async Task<Container> GetCosmosContainerAsync()
	{
		if (this.cosmosContainerCache == null) {
			...
			this.cosmosContainerCache = containerResponse.Container;
		}

		return this.cosmosContainerCache;
	}
}

In the same way, I had methods that could return null, and the caller was indeed checking it:

	public async Task<PodcastEpisode?> GetPodcastEpisodeAsync(string podcastId)
	{
		try {
			...
			PodcastEpisode result = itemResponse.Resource;
			return result;
		}
		catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
			return null;
		}
	}

	PodcastEpisode? episode = await cosmosHelper.GetPodcastEpisodeAsync(podcastId).ConfigureAwait(false);
	if (episode == null) {
		...
	}

In one case I have used throw expressions introduced in C# 7:

    afterRedirectUri = httpResponse.Headers.Location ?? throw new MyApplicationException("Location header missing");

Generally it works well. It's very annoying when the object has properties that are indeed set during object creation, through an object initializer - and even for init-only properties!
So, in reality, the above declaration of the Podcast2Download class is:

public class Podcast2Download
{
	// Id of the podcast, in format zoo_yyyyMMdd or 105polaroyd_yyyyMMdd
	public string Id { get; init; } = null!;
	public DateTime DateUtc { get; init; }
	public string FileName { get; init; } = null!;
	public Uri CompleteUri { get; init; } = null!;
}

Also I want to add: having nullable reference classes enabled doesn't remove the need to check that parameters of public methods are not null (CA1062).

Replaced Newtonsoft.Json with System.Text.Json

The previous version of my code used the excellent Newtonsoft.Json for object serialization and deserialization to/from JSON.
.NET Core 3 already introduced a built-in alternative, System.Text.Json.
I took the opportunity to migrate my code, and even in this case it has been very easy.

For one class, where I was using the JsonProperty attribute:

...
using Newtonsoft.Json;

	public class PodcastEpisode
	{
		// Id of the podcast, in format zoo_yyyyMMdd or 105polaroyd_yyyyMMdd
		[JsonProperty(PropertyName = "id")]
		public string Id { get; set; }
        ...
	}
}

it has been very easy to change it to JsonPropertyName:

...
using System.Text.Json.Serialization;

public class PodcastEpisode
{
	// Id of the podcast, in format zoo_yyyyMMdd or 105polaroyd_yyyyMMdd
	[JsonPropertyName("id")]
	public string Id { get; init; } = null!;
    ...
}

And the serialization and deserialization code has been also very easy to change:

using Newtonsoft.Json;

string serializedObj = JsonConvert.SerializeObject(episode);

Podcast2Download result = JsonConvert.DeserializeObject<Podcast2Download>(serialized);

to:

using JsonSerializer = System.Text.Json.JsonSerializer;

string serializedObj = JsonSerializer.Serialize(episode);

Podcast2Download result = JsonSerializer.Deserialize<Podcast2Download>(serialized)!

Note that I needed to explicitly import JsonSerializer from System.Text.Json, otherwise the compiler was still finding from the Newtonsoft.Json package, probably because imported by other packages.

Issue with CosmosDB SDK

Moving from Newtonsoft.Json to System.Text.Json has broken the serialization to CosmosDB.
In particular, even if I have used System.Text.Json.Serialization.JsonPropertyName instead of Newtonsoft.Json.JsonPropertyAttribute, this attribute was ignored and the (de)serialization was failing.
There is already a GitHub issue on this topic.
The temporary solution is to add a custom serializer, following this GitHub sample.

Updated Code Analysis

I have also updated Code Analysis following the new guidelines introduced in .NET 5.
I will describe this last point in another dedicated blog post.