Aller au contenu
Back.NET

Sortie de .NET 8, C# 12, Aspire... Bilan de la .NET Conf 2023

.NET 8 est maintenant disponible ! C'est l'occasion de faire le point sur les ajouts de cette nouvelle version.

Bilan de la .NET Conférence 2023 et la sortie de .NET 8 ©dotnetconf.net

Du 14 au 16 novembre derniers s'est déroulée la .NET Conf, conférence en ligne gratuite de trois jours, durant laquelle sont diffusées des sessions en direct animées par des experts de Microsoft et de la communauté .NET. Le thème de cette année était la sortie de .NET 8, la nouvelle version majeure du framework.

Beaucoup de sujets ont été présentés au cours de ces différentes sessions. Cet article vous propose un récapitulatif des principales nouveautés.

Simplifier la création d'applications cloud avec Aspire

Dévoilée lors de la keynote d'ouverture, .NET Aspire est une toute nouvelle stack de l'écosystème .NET, destinée à permettre de créer des applications distribuées optimisées pour le cloud. Elle est pour l'instant disponible en version preview.

Le template d'application Aspire permet de configurer l'orchestration de différents composants afin de créer des applications résilientes, observables, et configurables. Chaque composant sera lancé dans un conteneur Docker, afin de permettre la réalisation d'applications de type microservices.

Un composant Aspire est essentiellement une surcouche d'une dépendance (Project C#, Redis, RabbitMQ, base de données SQL, Key Vault Azure, etc.), qui permet de l'intégrer à son projet en la pré-configurant avec un certain nombre de fonctionnalités :

  • Mise à disposition d'un schéma JSON afin de permettre l'autocomplétion avec Intellisense de la configuration du composant dans appsettings.json.
  • Configuration de la résilience : retries, timeouts, et circuit breakers.
  • Configuration d'endpoints pour le health check.
  • Intégration de la journalisation et de la télémétrie (injection de ILogger, envoi de mesures, etc.).
  • Méthodes d'extensions qui configurent les services du composant dans l'injection de dépendance.

On évite ainsi d'avoir à installer différents packages pour chaque dépendance et d'effectuer la configuration soi-même.

Une solution Aspire se base sur deux projets centraux :

  • <appname>.AppHost : Gère l'orchestration des différents composants.
  • <appname>.ServiceDefaults : Projet dédié à contenir la configuration à appliquer à toutes les applications, via une méthode d'extension AddServiceDefaults. Celle-ci contient par défaut l'ajout de télémétrie, résilience (avec Polly), health checks, etc.

Aspire permet de composer les différents éléments de l'application directement depuis le code C#, dans le projet AppHost :

// == Program.cs, projet AppHost ==

var builder = DistributedApplication.CreateBuilder(args);

// Ajout d'un conteneur Redis nommé "cache"
var cache = builder.AddRedisContainer("cache");

// Ajout d'un projet API
var apiService = builder.AddProject<Projects.MyApi>("apiservice");

// Ajout d'un projet frontend, configuré pour utiliser 
// le conteneur Redis et l'API, définis comme dépendences.
builder.AddProject<Projects.MyFrontend>("frontend")
       .WithReference(cache)
       .WithReference(apiService);

// == Program.cs, projet MyFrontend ==

var builder = WebApplication.CreateBuilder(args);

// Ajout de la configuration commune
builder.AddServiceDefaults();

// Ajout d'une référence au conteneur redis
builder.AddRedisOutputCache("redis"); // 👈 Nom logique du conteneur

// Ajout d'un client pour l'API
builder.Services.AddHttpClient<ApiClient>(client => 
  client.BaseAddress = new("https://apiservice")) 
  // Pas de référence directe à une URL ! 👆

Le lien entre les composants est géré par la méthode WithReference : Pas de configuration de chaîne de connexion ou d'URLs, tout est géré par Aspire !

💡
L'utilisation de nom logique n'est pas spécifiquement liée à Aspire : Le package Microsoft.Extensions.ServiceDiscovery permet d'intégrer cette fonctionnalité à n'importe quel projet.
L'intégration à Aspire permet d'éviter de renseigner les endpoints des différents services manuellement dans la configuration.

Lancer le projet AppHost lance automatiquement tous les composants de l'application, ainsi qu'un tableau de bord qui permet de voir l'état des différents conteneurs, d’accéder à leurs journaux, ou bien d'inspecter le flux des requêtes entre les différents composants.
Le tableau de bord implémente un serveur OTLP (OpenTelemetry protocol), on peut donc également voir toutes les mesures envoyées.

0:00
/0:13

Présentation du dashboard pendant la keynote

Aspire est bien sûr intégré avec Azure : on peut donc créer les ressources adéquates et déployer l’ensemble des applications directement depuis la ligne de commande.

https://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/aca-deployment

Théoriquement, n'importe quel provider cloud peut être capable de déployer une application Aspire, grâce à la génération d'un manifeste. Ce fichier au format JSON contient toutes les informations connues par Aspire sur l'application : noms des ressources, configurations, variables d'environnement, etc.
Ce manifeste peut ensuite être utilisé pour déployer l'application, c'est d'ailleurs ce qu'utilise la ligne de commande Azure.

dotnet run --publisher manifest --output-path manifest.json

Beaucoup de nouveautés pour ASP.NET Core

Développement web Full Stack avec Blazor

Historiquement, Blazor propose deux modèles d’exécution :

  • Blazor WebAssembly : Comme son nom l'indique, l'application utilise WebAssembly pour s’exécuter dans le navigateur, sous forme de SPA.
  • Blazor Server : L'application est exécutée côté serveur, et les mises à jour de l'interface, la gestion des événements et les appels JavaScript sont gérés via une connexion SignalR, à l'aide du protocole WebSockets.

La version .NET 8 de Blazor abandonne ce double modèle pour proposer à la place un unique template d'application de type SSR (Static Server Rendering) qui en combine les fonctionnalités, permettant ainsi de réaliser des applications web Full Stack, appelé Blazor Web App.
Les composants générés sont statiques par défaut : l'ajout d'interactivité est "opt-in". Le choix de l’exécution côté serveur ou côté client sera défini via des modes de rendu ("render mode"), au niveau de chaque composant individuellement ou globalement à toute l'application.

4 modes sont proposés :

  • Static : Mode par défaut. Le code HTML du composant est généré côté serveur, et ne peut être mis à jour.
  • Interactive Server : Le composant est toujours généré côté serveur, mais des mises à jours sont possibles via une connexion SignalR avec le client. Cette connexion est automatiquement ouverte et fermée en fonction de l'affichage du composant interactif sur la page.
  • Interactive WebAssembly : Le composant est généré côté client avec Blazor WebAssembly. Le code du composant est téléchargé quand celui-ci est initialement affiché.
  • Interactive Auto : Le composant est initialement généré côté serveur, pendant que le runtime .NET WebAssembly et le code du composant sont téléchargés en arrière-plan. Lors des prochaines visites, le composant sera affiché côté client.

La mise en place de l'interactivité (par l'ouverture d'une connection WebSocket ou le téléchargement de code WebAssembly) ne se fait que si des composants en ont besoin, ce qui permet un chargement plus rapide des pages et un gain de performance.

Un système de navigation améliorée peut être activé avec l'inclusion du script blazor.web.js.
Plutôt que de lancer un rechargement complet de la page, Blazor va intercepter la requête et réaliser un appel fetch à la place, avant d'insérer le résultat de la réponse dans le DOM. On garde ainsi le statut actuel de la page, ce qui permet par exemple d'éviter de réinitialiser le défilement. Cette même logique peut être aussi appliquée à l'envoi de formulaires.

Toujours plus de réactivité avec le Streaming HTTP

Le Streaming Server-Side Rendering permet de réduire le temps de chargement de l'application en envoyant au client le contenu de la page au fur et à mesure, ce qui permet aux pages qui doivent exécuter des tâches asynchrones de longue durée (une requête en base de données par exemple) de se charger rapidement via une charge utile initiale de HTML avec un contenu temporaire.
Cette fonctionnalité peut être activée sur un composant avec l'attribut StreamingRendering.

0:00
/0:09

https://www.patterns.dev/react/streaming-ssr

Nouveaux composants

Deux nouveaux composants sont disponibles :

  • QuickGrid : Ce composant permet d'afficher des données sous forme de table, avec plusieurs fonctionnalités basiques (pagination, filtrage, tri, etc.). Trois types de source de données sont disponibles:
    • Données disponibles en mémoire via un IQueryable.
    • Base de données via Entity Framework Core.
    • Données distantes, via un ItemsProvider, qui ira récupérer des données depuis une API par exemple.
  • Sections : Les sections permettent d'afficher des composants dans des espaces définis sur une page, via les composants SectionOutlet et SectionContent.

Une meilleure DX pour l'écriture de routes

De nombreuses améliorations ont été apportées pour rendre l'écriture de routes d'API ou d'application MVC plus agréable dans l'IDE :

  • Les URLs des routes bénéficient maintenant d'une coloration et validation syntaxique des paramètres.
  • Les paramètres définis dans les routes sont autocomplétés dans les paramètres de méthodes, et vice versa.
  • Les contraintes de types sur les paramètres sont également autocomplétées.

Les attributs sur les routes de contrôleur sont désormais génériques, pour une syntaxe plus lisible.

// .NET 7
[ProducesResponseType(typeof(Todo), StatusCodes.Status200OK)]
public Todo Get()

// .NET 8
[ProducesResponseType<Todo>(StatusCodes.Status200OK)]
public Todo Get()

Dans Visual Studio, une nouvelle fenêtre "Endpoints explorer" permet de facilement voir la liste de toutes les routes de l'application.
Celles-ci sont découvertes de façon statique, il n'y a donc pas besoin de compiler l'application.

La dernière version de Visual Studio donne également accès à un éditeur HTTP, qui permet de créer des fichiers .http, dans lesquels on peut écrire des requêtes, pour ensuite les exécuter directement dans l'éditeur. Des requêtes peuvent être générées pour les endpoints de l'application directement depuis l'explorateur d'endpoints.

Ce nouvel éditeur permet de tester ses routes sans quitter l'IDE pour aller utiliser des outils comme Postman ou une interface Swagger par exemple.

Amélioration des informations de débugage

Certains types communément utilisés tirent maintenant partie de l'attribut DebuggerDisplay afin de rendre leurs propriétés plus lisibles.

Vue du débugueur pour HttpContext, .NET 7 vs .NET 8

L'accès aux informations importantes de l'application, comme les middlewares configurés, la liste des endpoints et leurs metadatas, le contenu de la configuration, ou bien encore les implémentations de ILogger utilisées, est rendue beaucoup plus simple.

De meilleures performances avec le Native AOT

Afin d'améliorer encore plus la performance des applications sur le cloud, ASP.NET Core 8 supporte désormais le déploiement Native AOT.
Cette option permet de compiler à l'avance ("Ahead-Of-Time") une application en code natif, ce qui évite de passer par l'utilisation d'un compilateur juste-à-temps (JIT) au runtime.

What is Just-In-Time(JIT) Compiler in .NET – En mode Native AOT, la compilation produit directement du code natif

Cela apporte plusieurs avantages:

  • La compilation produit un unique exécutable contenant le programme ainsi que le sous-ensemble de code des dépendances externes utilisées par le programme. On a donc une taille d'exécutable réduite, ce qui donne également des images de conteneurs plus petites et des temps de déploiement plus rapides.
  • Les applications démarrent plus vite, grâce à l'élimination de l'étape de compilation JIT, utilisent moins de mémoire, et n'ont pas besoin du runtime .NET pour s’exécuter.

Bien sûr il y a quelques limitations, notamment dû à l'optimisation du code qui peut rendre incompatible l'utilisation de la reflection ou du chargement dynamique d'assembly par exemple.

Afin de faciliter le découpage ("trimming") du code (dans le cadre d'AOT ou non), la classe WebApplication expose deux nouvelles méthodes en plus de CreateBuilder : CreateSlimBuilder et CreateEmptyBuilder, permettant d'initialiser son application avec différents niveaux de fonctionnalités.

Génération HTML de composants Razor hors d'ASP

Si jusqu'ici générer de l'HTML à partir d'un composant Razor n'était pas impossible, c'était quand même assez complexe à mettre en place.
En .NET 8, la nouvelle classe HtmlRenderer permet de faire ça assez simplement. Le but étant à terme de permettre la génération de site statique avec .NET.

// Mise en place...
IServiceCollection services = new ServiceCollection();
services.AddLogging();

IServiceProvider serviceProvider = services.BuildServiceProvider();
ILoggerFactory loggerFactory = serviceProvider
  .GetRequiredService<ILoggerFactory>();

await using var htmlRenderer = new HtmlRenderer(
  serviceProvider, 
  loggerFactory);

// Obligation de passer par un Dispatcher
var html = await htmlRenderer.Dispatcher.InvokeAsync(async () =>
{
    var dictionary = new Dictionary<string, object?>
    {
        { "Message", "Hello from the Render Message component!" }
    };

    var parameters = ParameterView.FromDictionary(dictionary);
    var output = await htmlRenderer
      .RenderComponentAsync<RenderMessage>(parameters);

    return output.ToHtmlString();
});

C'est toujours loin d'être intuitif et on a tout de même encore pas mal de code de mise en place, mais c'est déjà un pas en avant en comparaison des versions précédentes.

Nouvelles API pour l'authentification

La méthode MapIdentityApi ajoute différents endpoints à l'application (/register, /login, /confirmEmail, /account/info...) ce qui permet de pouvoir utiliser ASP.NET Core Identity via des appels API, sans avoir à passer par l'UI par défaut, ce qui n'était pas possible jusqu'ici.

Choisir les implémentations renvoyées par la DI

L'injection de dépendances via IServiceCollection donne maintenant la possibilité d'enregistrer des services identifiés par clé ("Keyed DI"). On peut ainsi définir plusieurs implémentations pour une même interface et indiquer avec un attribut laquelle récupérer.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) =>
  bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
  smallCache.Get("date"));

Différents cycles de vie sont bien sûr supportés, via AddKeyedSingleton, AddKeyedScoped et AddKeyedTransient.

Une nouvelle API pour les indicateurs

System.Diagnostics.Metrics est une nouvelle API de mesure de statistique, réalisée en collaboration avec le projet OpenTelemetry. Celle-ci ajoute de nouveaux éléments comme les histogrammes, centiles, ou encore les données multi-dimensionnelles.

Des tableaux de bords Grafana réalisés par l'équipe .NET sont disponibles pour facilement pouvoir visualiser toutes ces nouvelles données.

Une meilleure gestion des exceptions avec IExceptionHandler

Cette nouvelle interface permet d'ajouter des middlewares spécifiquement dédiés à la gestion des erreurs à son application, avec la méthode d'extension AddExceptionHandler<T>.

public class CustomExceptionHandler : IExceptionHandler
{
  private readonly ILogger<CustomExceptionHandler> logger;
  public CustomExceptionHandler(ILogger<CustomExceptionHandler> logger)
  {
    this.logger = logger;
  }
  
  public ValueTask<bool> TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken)
  {
    var exceptionMessage = exception.Message;
    logger.LogError(
      "Error Message: {exceptionMessage}, Time of occurrence {time}",
      exceptionMessage, DateTime.UtcNow);
      // On peut renvoyer false pour continuer l'execution,
      // ou true pour l'arrêter 
      // et indiquer qu'une exception a été traitée
      return ValueTask.FromResult(false);
  }
}

Entity Framework Core 8

Introduction des types complexes

Il est désormais possible de représenter des relations de composition (par exemple, un type Address qui serait inclus dans un type User) sans utiliser de Owned Types. On utilisera à la place des types complexes, qui à la différence des types entités n'ont pas de clé primaire. Une instance d'un type complexe peut donc être assignée à différentes propriétés.

var customer = new Customer
{
    Name = "Willow",
    Address = new() 
    { 
      Line1 = "Barking Gate", 
      City = "Walpole St Peter", 
      Country = "UK", 
      PostCode = "PE14 7AV" 
    }
};

context.Add(customer);
await context.SaveChangesAsync();

customer.Orders.Add(new Order 
{ 
  Contents = "Tesco Tasty Treats", 
  BillingAddress = customer.Address, 
  ShippingAddress = customer.Address, 
});

// En .NET 7, on aura ici une exception :
// "Cannot save instance of 'Order.ShippingAddress#Address' because it is 
// an owned entity without any reference to its owner. 
// Owned entities can only be saved as part of an aggregate 
// also including the owner entity."
await context.SaveChangesAsync();

La documentation recommande d'utiliser des records immutables pour représenter ce type d'objet.

public readonly record struct Address(
  string Line1,
  string City, 
  string Country, 
  string PostCode);

// Mise à jour de l'adresse
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };
await context.SaveChangesAsync();

Un type peut être défini comme complexe via l'attribut ComplexType, ou avec la méthode ComplexProperty() à la création du modèle.

Collections de primitives

La méthode usuelle pour stocker des collections de primitives (par exemple une liste de string) est de définir un convertisseur sur la propriété afin de la sérialiser vers une chaine de caractères dans la base de données.
Cette conversion est maintenant faite par défaut, avec de la sérialisation JSON.
Entity Framework peut utiliser les opérations JSON natives de la base de données pour effectuer des opérations à l'intérieur du JSON stocké.

public class Pub
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required string[] Beers { get; set; }
    public List<DateOnly> DaysVisited { get; private set; } = new();
}

var thisYear = DateTime.Now.Year;
var pubsVisitedThisYear = await context.Pubs
    .Where(e => e.DaysVisited.Any(v => v.Year == thisYear))
    .Select(e => e.Name)
    .ToListAsync();

DaysVisited sera stocké dans la base sous forme JSON

SELECT [p].[Name]
FROM [Pubs] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OPENJSON([p].[DaysVisited]) AS [d]
    WHERE DATEPART(year, CAST([d].[value] AS date)) = @__thisYear_0)

SQL généré pour SQL Server en réponse à la requête

Récupération de types non mappés

Faire des requêtes SQL pour récupérer des objets non configurés dans le contexte était impossible avant .NET 8. On peut maintenant récupérer n'importe quel objet depuis une requête :

var summaries =
    await context.Database.SqlQuery<PostSummary>(
            @$"SELECT 
              b.Name AS BlogName, 
              p.Title AS PostTitle, 
              p.PublishedOn
            FROM Posts AS p
            INNER JOIN Blogs AS b ON p.BlogId = b.Id")
        .ToListAsync();

Quoi de neuf dans C# 12 ?

Constructeur principaux pour les classes

Fonctionnalité déjà disponible pour les records, les constructeurs principaux sont maintenant également utilisables pour les classes et les structs.
Tout comme pour les records, les paramètres passés à ce constructeur principal sont disponibles dans le scope de la classe.

Un des avantages de cette nouvelle syntaxe est de réduire le boilerplate lors de l'utilisation de l'injection de dépendance:

// .NET 7
class School
{
    private readonly IStudentService studentService;
    private readonly ITeacherService teacherService;

    public School(
      IStudentService studentService, 
      ITeacherService teacherService)
    {
        this.studentService = studentService;
        this.teacherService = teacherService;
    }
}

// .NET 8
// ⚠️ Les paramètres ne sont pas readonly !
class School(
  IStudentService studentService, 
  ITeacherService teacherService)
{
}

Cependant, à la différence des records, les paramètres passés au constructeur principal ne génèrent pas automatiquement de propriétés publiques. Il faudra manuellement capturer le paramètre dans une propriété.

class Student(int id)
{
    public int Id { get; } = id;
}

Simplification de l'initialisation des collections

Les différents types de collection peuvent maintenant être initialisés avec une unique syntaxe (nommée "collection expression"), ce qui permet d'échanger les implémentations bien plus simplement.

// .NET 7
int[] array = new[] { 1, 2, 3 };
List<int> list = new() { 1, 2, 3 };
ImmutableList<int> immutableList = ImmutableList.Create(1, 2, 3);

int[] emptyArray = Array.Empty<int>();
List<int> emptyList = new();
IEnumerable<int> emptyEnumerable = Enumerable.Empty<int>();

// .NET 8
int[] array = [1, 2, 3];
List<int> list = [1, 2, 3];
ImmutableList<int> immutableList = [1, 2, 3];

int[] emptyArray = [];
List<int> emptyList = [];
IEnumerable<int> emptyEnumerable = [];

Il est aussi possible de créer ses propres types acceptant cette syntaxe via l'attribut CollectionBuilder. Celui-ci prendra en paramètre un type, qui sera responsable de créer la collection, ainsi que le nom de la méthode de création.

Plusieurs restrictions s'appliquent :

  • Le type de la collection doit être itérable, c'est à dire que l'on doit pouvoir en énumérer les éléments avec un foreach.
  • Le type contenant la méthode de création ne doit pas être un générique. Si la collection est générique, alors la méthode de création va devoir être définie sur un autre type, une classe de base ou une classe Builder par exemple.
  • La méthode de création doit être statique, accessible, renvoyer le type de la collection à créer et accepter uniquement un ReadOnlySpan<T> en paramètre.
[CollectionBuilder(typeof(MaCollection), nameof(Create))]
class MaCollection : IEnumerable<int>
{
    private readonly List<int> ints;

    public MaCollection(params int[] values)
    {
        this.ints = new List<int>(values);
    }

    public static MaCollection Create(ReadOnlySpan<int> items)
    {
        return new MaCollection(items.ToArray());
    }

    public IEnumerator<int> GetEnumerator() => ints.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => ints.GetEnumerator();
}

MaCollection maCollection = [1, 2, 3];

Comme on a vu dans un exemple précédent, cette syntaxe fonctionne aussi pour les interfaces ! Celles-ci peuvent implémenter l'attribut CollectionBuilder afin d'indiquer quel type concret instancier.

💡
Les types "core"
Certains types présents depuis longtemps dans le framework, comme List, Array, Span, ou certaines interfaces (IEnumerable, IList, ICollection...) n'ont pas besoin de l'attribut CollectionBuilder. Le compilateur saura comment les instancier.

Combinaison de collections avec le "spread element"

Cet opérateur, déjà utilisé dans le pattern matching de liste (appelé "slice pattern" dans ce contexte), permet de combiner très simplement des collections via une syntaxe similaire à celle du spread ... en javascript :

// .NET 7
int[] array = new[] { 1, 2 };
List<int> list = new() { 3, 4 };
IEnumerable<int> result = array
  .Concat(list)
  .Concat(new List<int>() { 5, 6 });

// .NET 8
int[] array = [1, 2];
List<int> list = [3, 4];
IEnumerable<int> result = [..array, ..list, 5, 6];

Extension des alias de type

Les alias peuvent maintenant être utilisés avec n'importe quel type, ce qui est très pratique notamment avec l'utilisation de Tuples.

// .NET 7
List<(int x, int y)> points;
points = new List<(int x, int y)>() { (1, 2), (3, 4) };

// .NET 8
using Point = (int x, int y);
List<Point> points;
points = new List<Point>() { (1, 2), (3, 4) };

Des images Docker plus légères et plus sécurisées

A partir de .NET 8, toutes les images Docker incluent un utilisateur non-root appelé app.
Le port 80 étant un port privilégié utilisable uniquement par root, le port par défaut pour les applications ASP devient 8080.

De nouvelles images Docker sont également disponibles, notamment des variantes Ubuntu chiselled (qui n'incluent pas de gestionnaire de paquets ou de shell) ou bien optimisées pour l'AOT.

💡
Depuis .NET 7, il est possible de publier ses applications en tant que container depuis la ligne de commande dotnet, sans Dockerfile. L'image de base la plus adaptée pour le projet sera choisie automatiquement.

Autres nouveautés apportées à .NET

Abstractions pour les dates

La classe TimeProvider et l'interface ITimer permettent de pouvoir (enfin !) mocker des opérations liées à la mesure du temps, sans avoir à écrire une interface soi-même. Par exemple :

  • Récupération de la date actuelle : DateTimeOffset.Now devient timeProvider.GetLocalNow(), et se base sur les propriétés virtuelles timeProvider.LocalTimeZone et timeProvider.GetUtcNow()
  • Création de timers avec timeProvider.CreateTimer
  • Attente avec Task.Delay ou Task.WaitAsync, qui acceptent un nouveau paramètre de type TimeProvider
  • Temps écoulé avec timeProvider.GetElaspedTime

TimeProvider étant une classe abstraite, on peut créer ses propres implémentations pour différents scénarios. Hors des tests, on utilisera la propriété statique TimeProvider.System.

Le package Microsoft.Extensions.Time.Testing fournit une classe FakeTimeProvider qui rend la mise en place d'un provider de test plus simple, par des méthodes d'initialisation comme SetLocalTimeZone ou SetUtcNow, mais aussi de contrôle de l'avancement du temps écoulé avec Advance.

Des nouvelles méthodes pour faire de l'aléatoire

Deux nouvelles méthodes facilitent la gestion de tableaux de valeurs aléatoires :

  • GetItems<T>, qui récupère un nombre donné d'éléments au hasard dans un tableau.
  • Shuffle<T>qui, comme son nom l'indique, mélange les valeurs du tableau.

Nouveaux types orientés performance 💪

Recherche plus efficace avec SearchValues<T> : Ce nouveau type est utilisé pour rechercher la première occurrence de certaines valeurs dans un Span. La valeur de recherche est mise en cache et optimisée au moment de l'appel à SearchValues.Create.

// .NET 7
string value = "foo bar";
char[] search = new[] { 'b', 'a', 'r' };
int position = value.IndexOfAny(search);

// .NET 8
string value = "foo bar";
SearchValues<char> search = SearchValues.Create("bar");
int position = value.AsSpan().IndexOfAny(search);

Les différentes méthodes de recherche sont supportées : IndexOfAny, LastIndexOfAny, IndexOfAnyInRange, IndexOfAnyExcept, etc.

Nouvelles collections immutables : Un nouveau namespace, System.Collections.Frozen, introduit deux nouveaux types de collection read-only : FrozenDictionary<TKey,TValue> et FrozenSet<T>.
À la différence des types existants ImmutableDictionary et ImmutableHashSet, ces nouveaux types sont plus coûteux à créer mais optimisés pour l'accès en lecture, et donc adaptés pour un cas d'usage où des collections sont initialisées au lancement de l'application et uniquement utilisées en lecture ensuite.

💡
En parlant d'immutabilité, les instances de JsonSerializerOptions peuvent maintenant être rendues read-only avec la méthode MakeReadOnly.

Mise en cache des chaînes de formatage : string.Format peut accepter un nouveau type CompositeFormat en remplacement de string. Instancié avec CompositeFormat.Parse, il permet d'optimiser la chaîne une seule fois plutôt que de la parser à chaque appel à string.Format.

Améliorations du parsing JSON

Plusieurs ajouts dans System.Net.Json :

  • L'option JsonNamingPolicy supporte maintenant le snake_case et  kebab-case.
  • Il devient possible de désérialiser des propriétés readonly grâce au paramètre PreferredObjectCreationHandling.
  • L'option JsonUnmappedMemberHandling permet de définir le comportement par défaut si des propriétés JSON n'existe pas dans le type .NET.
  • Sérialiser une interface dérivée inclut correctement les propriétés des interfaces de base.

Pour finir

Si vous avez manqué la .NET Conf 2023 ou si vous souhaitez revoir les sessions, vous pouvez les retrouver sur YouTube. Les supports de présentation de cette année et des précédentes sont aussi disponibles sur GitHub.
En plus des présentations liées à .NET 8, vous pourrez aussi trouver des sessions sur différents sujets comme la création de jeux en C# avec Godot, l'écriture d'Azure Functions, ou bien encore l'intégration de l'IA avec le SDK Semantic Kernel. N'hésitez pas à y jeter un œil !

Pour de la mise en pratique, une application démo de type eShop est disponible sur GitHub, pour servir de référence à la mise en place d'une architecture microservice avec .NET, en utilisant Aspire, Blazor, et MAUI.

Enfin, voilà une liste de liens pour aller creuser plus en profondeur sur la totalité de toutes les nouvelles fonctionnalités :

Dernier