10 useful .NET 8 features for Sitecore C# developer

.NET 8 released in November last year. So, this blog post is a bit late but better late than never. Also, bumped into this wikipedia  table about the list of .net version releases and associated support end date for different releases. So, this table should probably convey the importance of this post too.

Although there are many new features in .NET 8, the main purpose of this post is as a developer/blogger, I wanted to acquaint myself with the latest developer features useful on a day-to-day basis. So, in this process, I built my own examples for reference. Hope this is useful to you too and you enjoy this post. 


To test the features, first setup a project that supports .NET 8.0. In case of any nuget packages added, they must be in 8.x to support the latest features.


Note that some of the features like record is natively supported in .NET 8.0 framework version, while a framework version like .net 4.8 wouldn't. Interestingly, support for .NET 4.8 is indefinite!

Without much ado, here are some of the interesting features in .NET 8:

1. JsonInclude for non-public properties:

 

Serialising/Deserialising  C# objects to/from JSON string is part of any external application integration process. There seems to be a lot of enhancements in case of .NET 8 with regard to serialisation/deserialisation. One such feature is the ability of JsonInclude attribute to serialise/deserialise non-public members. Prior to .NET 8, only public properties could be decorated with this attribute.

Prior to .NET 8, such decoration throws an error as follows:

System.InvalidOperationException: 'The non-public property 'X' on type 'ConsoleApp2.MyPoco' is annotated with 'JsonIncludeAttribute' which is invalid.'

Usage:

//////

//////

Advantages:

- Better encapsulation of properties allowing value assignment through constructor

- Useful in passing calculated fields as part of json output

- Decorate C# properties with access modifiers like private,internal,protected, protected internal or private protected and the property will be automatically serialised. 

2. Stream-based ZipFile methods:

Support for stream-based options without creating the actual zip file - useful in optimising disk space - based on the stream itself, further processing can be decided. Here is an example that uses the two latest methods  - 

ZipFile.CreateFromDirectory
ZipFile.ExtractToDirectory

Prior to .NET 8, stream-based overload was not present and only string-based destination file name was supported with syntax error as follows:

Error CS1503 Argument 2: cannot convert from 'System.IO.Stream' to 'string'


Usage:

Create a zip stream for a directory input: From the stream create an unzipped directory in destination:
/////

The ability to pass stream as a param is one of the advantages of these new methods:

////


Advantages:

- Better way to interface/integrate between method calls
- Disk space optimisation since physical file with data isn't created unless needed

3. Serialise enum fields as strings - JsonStringEnumConverter<T> converter attribute

This is one of my favourite features - Prior to .NET 8, while serialising enum fields as json, the serialised enum fields are usually in integer format and as a result, there is usually a need to convert the integer to string before passing as json string. This is now resolved using JsonStringEnumConverter<T> converter attribute. Whatever fields you wish to pass as enum strings, just decorate it with [JsonConverter(typeof(JsonStringEnumConverter<T>))] and also ensure to create a JsonSerializerContext class and annotate it with the JsonSerializableAttribute attribute as depicted in the example code.

Prior to .NET 8:

Enum values get passed as integer in the json:


Usage:

.NET 8 JsonStringEnumConverter<T> converter attribute:

With JsonStringEnumConverter annotation:


Without JsonStringEnumConverter annotation:- Same as prior to .NET 8 behaviour:

Advantages:

- Better code readability
- Lesser lines of code/standardised approach
- Better integration with external applications 

Prior to .NET 8, this is the error you receive if using this attribute:

Error CS0308 The non-generic type 'JsonStringEnumConverter' cannot be used with type arguments

Usage:

 //****
//****

You could also decorate the partial context class with a blanket property - ([JsonSourceGenerationOptions(UseStringEnumConverter = true)]) in order to apply such conversion to all enum properties within the class as depicted below. Note the commented attributes above each of the individual properties since the class-level blanket property is in action:

4. Interface hierarchies:

Serialisation of inherited properties is possible with .NET 8. Here is an example to explain this feature better. Here is a hierarchical set of classes with interfaces.

Usage:

///////

/////

From .NET 8, json serialisation will look like this:

In other words, base class property values are retained in the child instance during serialisation.

Prior to .NET 8, with the same code, the serialisation happened this way and base property values are not present in the json:


In other words, although the cats are correctly differentiated based on fur in the child instance, they both seem not to inherit legs base property important for animals.

Advantages:

- Better inheritance in place

5. JsonNamingPolicy for snake case and kebab case:

Prior to .NET 8 only camel-case was supported for JSON field serialisation:


From .NET 8
, multiple options for Kebab and snake case are available:

- SnakeCaseUpper

- SnakeCaseLower

- KebabCaseUpper

- KebabCaseLower


Advantages:

- While integrating with external applications, although camelcase is predominantly used, a few applications have the other two naming conventions in use so, this support is useful for future

6. FrozenDictionary

One of the interesting additions is the System.Collections.Frozen namespace that has the FrozenDictionary and this is useful if you want to load a list that wouldn't change after first time load. The FrozenDictionary offers faster traversal/retrieval of key/value pairs.

Prior to .NET 8, no such namespace:


From .NET 8, Frozen is a namespace to utilise FrozenDictionary:


Usage:

A method that builds a dictionary and returns a FrozenDictionary:

/////////

////////

Method call:

Once the FrozenDictionary is retrieved, you can get value from the dictionary but can't add any more key/value pairs:


Trying to add a key/value pair fails after initial load since the dictionary is frozen:

System.NotSupportedException

  HResult=0x80131515

  Message=Specified method is not supported.

  Source=System.Collections.Immutable

  StackTrace:

   at System.Collections.Frozen.FrozenDictionary`2.System.Collections.Generic.IDictionary<TKey,TValue>.Add(TKey key, TValue value)

   at System.Collections.Generic.CollectionExtensions.TryAdd[TKey,TValue](IDictionary`2 dictionary, TKey key, TValue value)

  

Advantages:

- Better performance for large key/value pairs, could be useful to load Sitecore dictionary

7. Keyed DI services

This again is one of the exciting new Dependency Injection features wherein when you have multiple implementations of the same interface, you can use any of those implementations by just passing the necessary key. 

Usage:

For the sake of example, based on my earlier blog posts regarding different sitemap implementations for Sitecore MVP site that currently runs on .NET 8, I could provide keys and differentiate two different implementations and add it as part of the service collection:

Now, I have the option to use any of these implementations or both in my code. Here below, I have just commented the old implementation and injected the service provider to use one of the implementations:

Advantages:

- Flexibility in using multiple implementations of the same interface

- Might not require code changes in case of switching implementations if all implementations are identified and have keys before hand then, switching can be done by just passing the necessary key as a string from the config file

8. Additional methods in HostedlifecycleServices:

The following methods are added to the life cycle apart from the existing ones as depicted in the diagram. So, a worker class can be created and these methods can hold logic to execute during the starting or stopping process of the service.

StartingAsync(CancellationToken)
StartedAsync(CancellationToken)
StoppingAsync(CancellationToken)
StoppedAsync(CancellationToken)

Usage:

Simple counter increment implemented in a worker class while the methods are useful in setting up the variable and cleanup as part of the stop process.

Worker class that implements the IHostedLifecycleService:
/////
/////

Program.cs that invokes the above worker logic:
///////


///////

Advantages:

- Better control/logging over the application service life cycle/process

9. Options Validation Source generator:

This option is useful in firing validation issues right during application startup. To understand this better, here is the usage scenario. Although this option is good to be used for technical information like connection timeout etc., in case of this example, the idea is to catch under-age voter data settings. 

Usage:

Define a class that will hold voter data:

///////*******

//////*******

Define the centralised logic for validation:

- Validate if voter age is less than 18
- Validate if postal code doesn't start with 3

///////%%%%%
//////%%%%%

Associated appSettings.json:

//////######
//////######

Wiring up the settings and validation together in Startup.cs:

The validation fires when the view is rendered:



Advantages:

- Centralised validation handler
- Catching issues early during application life cycle


10. Validation attributes

A few validation attributes like AllowedValues, DeniedValues are added anew in .NET 8. Here is an example of usage of AllowedValues:

Usage:

Model decorated with AllowedValues attribute:
//*****

///*****

Program.cs:

///%%%%
//%%%%

Comments