C# 12 Features
- Inleiding
- .Net 8 en C# 12
- Upgrade
- Keyed Services
- Default Parameters in Lambda Expressions
- Alias Any Type
- Primary Constructors
- InLine Arrays
- Collection Expressions en de Spread-operator
- Slot
Inleiding
Microsoft komt om de zoveel tijd met een nieuwe versie van C# en zo is in november 2023 .Net 8 met C# 12 uitgebracht.
We laten in deze post een aantal features de revue passeren die nieuw zijn in .Net 8 / C# 12. Features waar we het over gaan hebben in deze post zijn Keyed Services, default parameters in Lambda Expressions, de Alias any type, Primary Constructors, InLine Arrays en Collection Expressions met de Spread-operator.
Het één en ander aan wordt besproken aan de hand van een .Net 8 C# 12 Minimal API/WASM-app en een Console app. De voorbeeldcode kun je hier vinden:
- Repository Minimal Web API-solution (de backend)
- Repository WASM (WebAssembly) client-solution (de frontend)
- Repository Console App
.Net 8 en C# 12
Het verkrijgen van .Net 8 met C# 12 gaat vrij eenvoudig als je Visual Studio 2022 (of hoger) gebruikt. Je krijg er namelijk automatisch .Net 8 met C# 12 bij zodra je Visual Studio 2022 bijwerkt naar versie 17.8.0 of hoger.
In Visual Studio 2022 heb je menukeuze Help > Check for Updates waarmee je kunt zien of er nieuwe versies van Visual Studio zijn:
De versie die we in deze post gebruiken:
Ga voor het doen installeren van .Net 8 naar de website van Microsoft als je geen Visual Studio mocht hebben en een andere IDE gebruikt:
Uiteindelijk kun je als volgt zien of de benodigde SDK (Software Development Kit) op je computer aanwezig is: dotnet –list-sdks
Upgrade
We hebben in deze post over Blazor CRUD minimal API gebruik gemaakt van een ASP .NET Core Minimal Web API-solution (de backend) en een WASM (WebAssembly) client-solution (de frontend).
De solutions gaan we üpgraden naar .Net 8 waarna we de nieuwe .Net8-features gaan bekijken aan de hand van de geüpgrade solutions. De broncode kun je hier (frontend) en hier (backend) terugvinden.
De Target framework wijzigen we van .NET 6.0 naar .NET 8.0:
De NuGet-packages üpgraden we ook maar meteen voor de WASM (WebAssembly) client-solution (de frontend):
waarbij we de versienummers van het geïnstalleerde terugzien in het projectbestand:
En voor de Minimal Web API-solution (de backend) doen we ook een üpgrade:
waarbij we de versienummers van het geïnstalleerde terugzien in het projectbestand:
Keyed Services
We hebben in de ASP .NET Minimal Web API-solution (de backend) een IModel-interface. De interface heeft twee implementaties.
Zo is er een implementatie waarbij alles uit een List<T> uit het intern geheugen wordt gehaald (MemoryModel) en een implementatie waarbij alles uit een SQL Server database wordt gehaald (DBModel).
De Keyed Services-feature is een nieuwe .Net 8 / C# 12-feature waarmee we een implementatie kunnen registreren. Zo kunnen we in onderstaand voorbeeld voor elke Minimal API aangeven welke implementatie gebruikt mag worden. Daarbij geven we een sleutelwaarde (key) op voor de te gebruiken implementatie.
In bovenstaand voorbeeld wordt de MemoryModel-implementatie (“memory”) gebruikt voor Minimal API /HaalOpEigenaren terwijl voor de andere Minimal APIs de DBModel-implementatie (“DB”) wordt gebruikt.
Bovenstaand is niet helemaal logisch, want je zal alles óf uit de List<T> uit het intern geheugen halen óf uit een SQL Server database en niet uit beide gegevensbronnen.
Er zijn desalniettemin toch scenario’s te bedenken dat je voor bepaalde Minimal APIs voor de beste performance een implementatie gebruikt waarbij alles uit een cache wordt gehaald, terwijl je voor andere Minimal APIs dat niet wil of niet kunt doen en dan een implementatie moet gebruiken waarbij alles uit een database wordt opgehaald.
Default Parameters in Lambda Expressions
Default waarden kunnen met .Net 8 / C# 12 gebruikt worden in Lambda Expressions.
Het één en ander kan geïllustreerd worden met Minimal API /HaalOpEigenaar. De Minimal API haalt aan de hand van een ID gegevens op en …
we kunnen bij de parameter een defaultwaarde opgeven dat gebruikt wordt indien niets is opgegeven bij de aanroep van de Minimal API (regel 3 van de code snippet):
app.MapGet("HaalOpEigenaar/{ID}",
async ([FromKeyedServices("memory")] IModel model,
int ID = 5) =>
{
try
{
return Results.Ok(await model
.HaalopEigenaar(ID) is EIGENAAR eigenaar ?
Results.Ok(eigenaar) :
Results.NotFound("Niks gevonden"));
}
catch (Exception ex)
{
return Results
.Problem("Er is iets fout gegaan
met minimal API HaalOpEigenaar - "
+ ex.Message);
}
});
In dit voorbeeld gebruiken we een defaultwaarde 5 wat als zodanig wordt getoond in Swagger:
Zie verder deze post van Thomas Claudius Huber waarin verder wordt ingegaan op default waarden bij methoden en default waarden bij Lambda expressions.
Alias Any Type
In C# kan een alias gebruikt worden voor o.a. namespaces en men heeft het gebruik van de Alias in .Net 8 / C# 12 uitgebreid. Zo kan een alias gebruikt worden voor Tuples, Pointers en Arrays.
De ASP .NET Minimal Web API-solution (de backend) gebruikt voor haar MemoryModel-implementatie een List<T> en voor die List<T> gaan we een alias (DeLijst) gebruiken:
// Alias Array Types
using DeLijst =
System.Collections.Generic.List<EIGENAAR>;
En we kunnen alias DeLijst als volgt gebruiken:
private readonly DeLijst _Eigenaren =
[
new EIGENAAR()
{ ID = 1, Omschrijving = "...", "... },
new EIGENAAR()
{ ID = 2, Omschrijving= "...", "... },
...
new EIGENAAR()
{ ID =24, Omschrijving="...", "...}
];
...
// Zonder Alias:
// public async Task<List<EIGENAAR>>
// HaalOpEigenaren()
public async Task<DeLijst> HaalOpEigenaren()
{
return await Task.Run(() => _Eigenaren);
}
Zie verder deze post op dev.to van Michael Jolley en deze post van Thomas Claudius Huber waarin meer in detail wordt ingegaan op de Alias Any Type.
De volgende features zullen we toelichten aan de hand van een Console App en de broncode kun je hier terugvinden.
Primary Constructors
Primary Constructors waren alleen voorbehouden aan record types. Dit hebben ze in .Net 8 / C# 12 uitgebreid. We kunnen de primary constructor nu ook voor klassen en structs gebruiken.
We definiëren in een Console App een klasse waarbij geen gebruik wordt gemaakt van een primary constructor. We zien dat we alle, via de “gewone” constructor binnengekomen parameterwaarden moeten overzetten naar backing variables.
Methode ToonGegevens() moet gebruik maken van die backing variables. De, via de “gewone” constructor binnenkomende parameterwaarden zijn alleen toegankelijk voor de (“gewone”) constuctor:
public class EIGENAAR1
{
// Backing variables
private readonly int? _ID;
private readonly string? _Omschrijving;
private readonly string? _Voornaam;
private readonly string? _Achternaam;
private readonly string? _Regio;
public EIGENAAR1(
int? ID,
string? Omschrijving,
string? Voornaam,
string? Achternaam,
string? Regio)
{
// Constructor
_ID = ID;
_Omschrijving = Omschrijving;
_Voornaam = Voornaam;
_Achternaam = Achternaam;
_Regio = Regio;
}
public void ToonGegevens()
{
// Alles wat via de constructor binnenkomt
// moet je overzetten naar Backing variables
// om het te kunnen gebruiken
Console.WriteLine($"\nID:{_ID}
\nOmschrijving:{_Omschrijving}
\nVoornaam:{_Voornaam}
\nAchternaam:{_Achternaam}
\nRegio:{_Regio}");
}
}
Backing variables zijn met een primary constructor niet meer nodig. Je ziet dat je code met een primary constructor weer wat compacter wordt:
public class EIGENAAR2(
int? ID,
string? Omschrijving,
string? Voornaam,
string? Achternaam,
string? Regio)
{
public void ToonGegevens()
{
// Je kunt de parameters van de
// Primary Constructor meteen gebruiken
Console.WriteLine($"\nID: {ID}
\nOmschrijving:{Omschrijving}
\nVoornaam:{Voornaam}
\nAchternaam:{Achternaam}
\nRegio:{Regio}");
}
}
We instantiëren wat objecten:
// Constructor
EIGENAAR1 eigenaar1 = new(
1, "Sandra's auto", "Sandra", "Janssen", "Noord");
eigenaar1.ToonGegevens();
// Primary Constructor
EIGENAAR2 eigenaar2 = new(
2, "Peter's auto", "Peter", "Veerman", "Midden");
eigenaar2.ToonGegevens();
En we krijgen dit resultaat:
InLine Arrays
Je kunt met .Net 8 / C# 12 gebruik maken van Inline Arrays als je gegevens ergens in het geheugen wil opslaan en optimale performance een “must” is waarbij die ene milliseconde snelheidswinst van groot belang is.
using System.Runtime.CompilerServices;
var InLineArray = new InLineArrayStruct();
InLineArray[0] = 100;
InLineArray[1] = 200;
Console.WriteLine("InLineArray:");
Console.WriteLine
($"-InLineArray[0]:{InLineArray[0]}");
Console.WriteLine
($"-InLineArray[1]:{InLineArray[1]}");
[System.Runtime.CompilerServices.InlineArray(2)]
public struct InLineArrayStruct
{
private int? _ID;
}
Je creëert een inline array met een vaste grootte en een struct. De compiler genereert hiermee optimale IL (Intermediate Language) code omdat de grootte van te voren bekend is.
Verder wordt met een struct (value type) de stack gebruikt i.p.v. de heap wat weer gunstiger is voor de performance. Bovenstaande code geeft onderstaand resultaat:
Collection Expressions en de Spread-operator
Een array kan als volgt gedeclareerd worden:
var arrayOud = new int[] { 1, 2, 3 };
maar deze collection expression-syntax was ook mogelijk en die syntax werd door velen als prettiger ervaren:
int[] arrayCollectionExpression =
[1, 2, 3];
In .Net 8 / C# 12 heeft men de collectie expression-syntax uitgebreid naar de List<T>- en de span<T>-collecties waarmee dit mogelijk is:
Span<int> spanCollectionExpression =
[10, 20, 30];
List<int> listCollectionExpression =
[100, 200, 300];
In .Net 8 / C# 12 heeft men ook de spread-operator (de “..”) geïntroduceerd waarmee je collecties kunt samenvoegen tot een nieuwe collectie en je ook nog wat extra’s (in dit geval de getallen 1000 en 2000) kan toevoegen aan die nieuwe collectie:
List<int> collectieSpreadOperator =
[
.. arrayCollectionExpression,
.. spanCollectionExpression,
.. listCollectionExpression,
1000, 2000
];
Console.WriteLine("collectieSpreadOperator:");
foreach (int i in collectieSpreadOperator)
{
Console.WriteLine(i);
}
Alles is samengevoegd tot een List<T>-collectie, maar de nieuwe collectie kan ook een array[] of een span<T> zijn. Bovenstaande code geeft onderstaand resultaat:
Slot
Een aantal .Net 8 / C# 12 features hebben we in deze post besproken. Voor elke nieuwe release van C# kan weer de vraag gesteld worden of C# developers iets met die nieuwe features gaan doen.
Gaan ze inderdaad de features gebruiken om zo te komen tot beter te doorgronden code? Of is een kleine verandering in de manier van programmeren al te veel gevraagd waarbij vasthouden aan een cryptische, voor anderen niet te doorgronden programmeerstijl toch gemakkelijker is? Ik sta zelf niet onwelwillend tegenover de nieuwe features en ik zal ze zeker gebruiken als ze de leesbaarheid van mijn code verbeteren.
Hopelijk ben je met deze posting weer wat wijzer geworden en ik hoop je weer terug te zien in één van mijn volgende blog posts. Wil je weten wat ik nog meer over C# heb geschreven? Hit the C# button…