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.
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:
//////
internal class AppLogic | |
{ | |
internal AppLogic() { } | |
[JsonConstructor] | |
internal AppLogic(int length, int breadth) | |
{ | |
Length = length; | |
Breadth=breadth; | |
Area = length * breadth; | |
} | |
[JsonInclude] | |
internal int Length { get; set; } | |
[JsonInclude] | |
internal int Breadth { get; set; } | |
[JsonInclude] | |
private int Area { get; }//include calculated field in json output | |
} |
//////
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.
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'
Create a zip stream for a directory input:
internal Stream GetZipStream(string inputDirectory) | |
{ | |
var sav = "test.zip"; //blank zip | |
Stream stream = new FileStream(sav, FileMode.OpenOrCreate, FileAccess.ReadWrite); // just opening the blank stream | |
ZipFile.CreateFromDirectory(inputDirectory, stream); //load the stream from contents of diff directory | |
return stream; | |
} |
internal void CreateArtifactFromStream(Stream memoryStream,string outputDirectory) | |
{ | |
ZipFile.ExtractToDirectory(memoryStream, outputDirectory); | |
} |
The ability to pass stream as a param is one of the advantages of these new methods:
AppLogic logic = new(); | |
Stream stream= logic.GetZipStream("c:\\vvv");//create zip stream of the directory | |
logic.CreateArtifactFromStream(stream, "c:\\empty");//from the stream, unzip contents in destination |
////
[JsonSerializable(typeof(CityMonthClimate))] | |
public partial class Context1 : JsonSerializerContext { } | |
public class CityMonthClimate | |
{ | |
public string? Country { get; set; } | |
public string? City { get; set; } | |
public Month? Month { get; set; } | |
public Climate? Climate { get; set; } | |
} | |
[JsonConverter(typeof(JsonStringEnumConverter<Climate>))] | |
public enum Climate | |
{ | |
Summer, Winter, Autumn, Spring,AllinOne | |
} | |
[JsonConverter(typeof(JsonStringEnumConverter<Month>))] | |
public enum Month | |
{ | |
January, February, March, April, May, June, July, August, September,October, November,December | |
} |
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:
///////
public interface IAnimal | |
{ | |
public int Legs { get; set; } | |
} | |
public interface ICat : IAnimal | |
{ | |
public bool Furry { get; set; } | |
} | |
public class Siamese : ICat | |
{ | |
public bool Furry { get; set; } | |
public int Legs { get; set; } | |
} | |
public class Persian : ICat | |
{ | |
public bool Furry { get; set; } | |
public int Legs { get; set; } | |
} |
/////
From .NET 8, json serialisation will look like this:
In other words, base class property values are retained in the child instance during serialisation.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.
5. JsonNamingPolicy for snake case and kebab case:
Prior to .NET 8 only camel-case was supported for JSON field serialisation:
- 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:
/////////
private static readonly FrozenDictionary<string, string> s_configurationData = LoadConfigurationData(); | |
private static FrozenDictionary<string,string> LoadConfigurationData() | |
{ | |
Dictionary<string, string> dict = new Dictionary<string, string> | |
{ | |
{ "1", "navan" }, | |
{ "2", "z" }, | |
{ "3", "y" } | |
}; | |
return dict.ToFrozenDictionary(); | |
} |
////////
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:
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
using System.Text.Json; | |
using WorkerTimer.Models; | |
namespace WorkerTimer; | |
//https://github.com/renatogroffe/DotNet8-WorkerService-IHostedLifecycleService | |
public class Worker : IHostedLifecycleService | |
{ | |
private readonly ILogger<Worker> _logger; | |
private readonly ApplicationState _applicationState; | |
private Timer? _timer; | |
private int Counter { get; set; } | |
public Worker(ILogger<Worker> logger) | |
{ | |
_logger = logger; | |
_logger.LogInformation("***** IHostedLifecycleService *****"); | |
_applicationState = new ApplicationState(); | |
DisplayCurrentTime(_applicationState); | |
LogApplicationStatus("Constructor"); | |
} | |
public Task StartingAsync(CancellationToken cancellationToken) | |
{ | |
_applicationState.StartingAsync = true; | |
Counter = 100; | |
LogApplicationStatus(nameof(StartingAsync)); | |
return Task.CompletedTask; | |
} | |
public Task StartAsync(CancellationToken cancellationToken) | |
{ | |
_applicationState.StartAsync = true; | |
Counter = 200; | |
LogApplicationStatus(nameof(StartAsync)); | |
_logger.LogWarning("Press Ctrl+C to stop worker..."); | |
return Task.CompletedTask; | |
} | |
public Task StartedAsync(CancellationToken cancellationToken) | |
{ | |
_applicationState.StartedAsync = true; | |
for (int i = Counter; i <= 100000; i++) | |
{ Console.WriteLine(i); } | |
Counter = 100000; | |
LogApplicationStatus(nameof(StartedAsync)); | |
return Task.CompletedTask; | |
} | |
public Task StoppingAsync(CancellationToken cancellationToken) | |
{ | |
Console.WriteLine("Counter countdown stopping..."); | |
_applicationState.StoppingAsync = true; | |
Counter = 2; | |
LogApplicationStatus(nameof(StoppingAsync)); | |
return Task.CompletedTask; | |
} | |
public Task StopAsync(CancellationToken cancellationToken) | |
{ | |
_applicationState.StopAsync = true; | |
Counter = 1; | |
LogApplicationStatus(nameof(StopAsync)); | |
return Task.CompletedTask; | |
} | |
public Task StoppedAsync(CancellationToken cancellationToken) | |
{ | |
_applicationState.StoppedAsync = true; | |
Counter = 0; | |
LogApplicationStatus(nameof(StoppedAsync)); | |
return Task.CompletedTask; | |
} | |
private void DisplayCurrentTime(object? state) => _logger.LogInformation( | |
$"Actual time: {DateTime.Now:HH:mm:ss} | " + | |
$"Worker status: {JsonSerializer.Serialize(state)} | Counter:{Counter}"); | |
private void LogApplicationStatus(string methodName) => _logger.LogInformation( | |
$"{methodName}| Counter:{Counter} | Worker status: {JsonSerializer.Serialize(_applicationState)}"); | |
} |
using WorkerTimer; | |
var builder = Host.CreateApplicationBuilder(args); | |
builder.Services.AddHostedService<Worker>(); | |
var host = builder.Build(); | |
host.Run(); |
namespace OptionsNetCore.Core.Options.Voter | |
{ | |
public class VoterOptions | |
{ | |
public const string Voter = "VoterSettings"; | |
public int Age { get; set; } | |
public string Name { get; set; } | |
public string Suburb { get; set; } | |
public string City { get; set; } | |
public string PostalCode { get; set; } | |
} | |
} |
using Microsoft.Extensions.Options; | |
using OptionsNetCore.Core.Options.Voter; | |
namespace OptionsNetCore.Core.Options.Voter | |
{ | |
public class VoterValidation : IValidateOptions<VoterOptions> | |
{ | |
public ValidateOptionsResult Validate(string name, VoterOptions options) | |
{ | |
string failures = null; | |
if (options.Age <18) | |
failures = "Voting age must be atleast 18;"; | |
if (!options.PostalCode.StartsWith('3')) | |
failures = "Postal Code must start with 3 for this city;"; | |
if (!string.IsNullOrEmpty(failures)) | |
return ValidateOptionsResult.Fail(failures); | |
return ValidateOptionsResult.Success; | |
} | |
} | |
} |
{ | |
"Logging": { | |
"LogLevel": { | |
"Default": "Information", | |
"Microsoft": "Error", | |
"Microsoft.Hosting.Lifetime": "Error" | |
} | |
}, | |
"AllowedHosts": "*", | |
"VoterSettings": { | |
"Age": 12, | |
"City": "Melbourne", | |
"PostalCode": "4030" | |
} | |
} |
using System.ComponentModel.DataAnnotations; | |
namespace ConsoleAppAllowedValues.Models; | |
public class State | |
{ | |
[Required] | |
[AllowedValues("VIC", "NSW", "SA", "WA", "NT", "TAS", ErrorMessage = "Invalid State Code")] | |
public string? StateCode { get; set; } | |
[Required] | |
public string? Name { get; set; } | |
} |
using System.ComponentModel.DataAnnotations; | |
using System.Text.Json; | |
using ConsoleAppAllowedValues.Models; | |
Console.WriteLine("*****.NET 8 | AllowedValuesAttribute *****"); | |
var states = new State[] | |
{ | |
new() {StateCode = "VIC", Name = "Victoria"}, | |
new() {StateCode = "VOC", Name = "Victoria"}, | |
new() {StateCode = "TAS", Name = "Tasmania"}, | |
new() {StateCode = "NSW", Name = "New South Wales"}, | |
new() {StateCode = "NT", Name = "Northern Territory"}, | |
new() {StateCode = "WA", Name = "Western Aus"}, | |
new() {StateCode = "SA", Name = "South Australia"} | |
}; | |
foreach (var state in states) | |
{ | |
Console.WriteLine(); | |
Console.WriteLine(JsonSerializer.Serialize(state)); | |
var validationResults = new List<ValidationResult>(); | |
if (!Validator.TryValidateObject(state, new ValidationContext(state), | |
validationResults, validateAllProperties: true)) | |
{ | |
Console.WriteLine("Invalid state(s)..."); | |
foreach (var validationResult in validationResults) | |
Console.WriteLine($"ErrorMessage = {validationResult.ErrorMessage}"); | |
} | |
} |
Comments
Post a Comment