Comme tous les ans, l’édition de la .NET Conf s’est tenue du 14 au 16 novembre 2023 (oui je sais, je suis en retard…). Cette année a été un bon cru côté annonces et nouveautés.

De mon point de vue, la plus grosse annonce de Microsoft a probablement été .NET Aspire. Etant donné qu’il y a pas mal de choses à dire à son sujet, je lui ai réservé un article dédié qui sortira bientôt…

En attendant, prenons le temps de parcourir les autres sujets que j’ai trouvés intéressants !

Attention, l’idée n’est pas de rentrer dans le détail de tous les sujets, mais de vous donner une vision globale ainsi que des liens vers des ressources supplémentaires pour aller creuser certains sujets si vous le souhaitez.

Table des matières

.NET 8

Comme traditionnellement, c’est l’occasion pour Microsoft d’annoncer la version 8 du framework .NET. Selon le nouveau cycle de release, il s’agit donc d’une version LTS (numéro pair), qui sera supportée jusqu’en novembre 2026 (3 ans).

.NET Momentum :

  • 6.1+ million d’utilisateurs actifs par mois
  • 53000+ membres de la communauté ont contribués à .NET
  • 1ère place 2023 - Stack Overflow most popular technologies (catégorie other framework)
  • Top 5 des langages de développement les plus utilisés sur GitHub

Comme depuis .NET 5, le focus est particulièrement mis sur les performances, avec plus de 1250 améliorations.

Mais cette nouvelle version est aussi l’occasion d’apporter des nouvelles fonctionnalités ou d’en améliorer des existantes.

TimeProvider

Microsoft a enfin intégré dans son framework une classe d’abstraction pour la gestion du temps : TimeProvider !

Cette classe permet de résoudre de nombreux problèmes, notamment lorsque l’on veut simuler du temps dans des scénarios de test ou que l’on souhaite avoir un fournisseur de temps dans un fuseau horaire différent.

Via le package Microsoft.Extensions.TimeProvider.Testing, on a accès à une implémentation de TimeProvider : FakeTimeProvider. Avec cette classe, on peut aller jusqu’à pouvoir avancer l’horloge manuellement pour contrôler le temps !

var fakeTime = new FakeTimeProvider(startDateTime: DateTimeOffset.UtcNow);
fakeTime.Advance(TimeSpan.FromSeconds(1));
fakeTime.Advance(TimeSpan.FromMilliseconds(500));

System.Random

Quand on a besoin d’avoir des comportements pseudo-aléatoire, la classe System.Random rend de grands services. Avec .NET 8, elle s’enrichit de deux méthodes concernant les collections.

GetItems<T>() permet de choisir, de manière aléatoire, un nombre d’éléments dans une collection. Dans cet exemple, je souhaite générer un tableau de 10 fruits en piochant aléatoirement à partir de mon référentiel.

var fruits = new[]
{
    "apple",
    "banana",
    "orange",
    "strawberry",
    "kiwi"
};
var selectedFruits = Random.Shared.GetItems<string>(fruits, 10);

Shuffle<T>() permet de trier, de manière aléatoire, une collection.

var items = new[]
{
    "first",
    "second",
    "third",
    "fourth",
    "fifth",
};
Random.Shared.Shuffle(items);

System.Collection.Frozen

.NET 8 apporte de nouveaux types axés sur les performances. On peut notamment noter citer FrozenDictionary<TKey, TValue> et FrozenSet<T>. Une fois la collection créée, ces objets n’autorisent plus de modification. En contrepartie, ils offrent des performances sur les opérations de lecture supérieures. Cela peut-être très pratique pour des collections de type référentiel (chargée à la première utilisation puis conservée sur une longue durée).

A noter aussi System.Buffers.SearchValues<T> et System.Text.CompositeFormat.

Keyed Service

Les Keyed Service (aka. Named Service) permettent d’ajouter un service dans l’injecteur de dépendances en utilisant une clé en plus du classique couple interface/implémentation.

L’enregistrement sera donc identifié par le couple clé/ServiceType.

C’est très pratique dans le cas où j’ai une interface avec plusieurs implémentations.

Cela existait déjà dans d’autres framework d’injection de dépendances, comme StructureMap et Autofac, mais c’est désormais intégré built-in dans le framework .NET !

Avant .NET 8 : 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IService, MyFirstService>();
builder.Services.AddSingleton<IService, MySecondService>();
builder.Services.AddSingleton<IService, MyThirdService>();

// Je récupère tous les services 
public class MyClass(IEnumerable<IService> services) {}

// Je récupère uniquement le dernier service enregistré
public class MyClass(IService service) {}

Avec .NET 8 : 
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<IService, MyFirstService>("first");
builder.Services.AddKeyedSingleton<IService, MySecondService>("second");
builder.Services.AddKeye

public class MyClass([FromKeyedService]("second") IService secondService) {}

Pour simplifier la gestion et limiter les erreurs, je vous conseille d’utiliser des énumérations pour gérer vos clés ;)

Je vous invite à jeter un coup d’oeil à cet article, qui donne notamment quelques cas d’utilisation très intéressant.

Hosted Service

Dans un Hosted Service, on a désormais accès à d’autres options d’exécution durant le cycle de vie. En plus des traditionnels StartAsync et StopAsync, .NET 8 fournit maintenant :

Starting et Stopping s’exécutent donc respectivement avant Start et Stop, et Started et Stopped après.

Cela offre plus de possibilité dans le développement de nos services hébergés.

Si vous souhaitez migrer votre application vers cette nouvelle version, attention aux quelques breaking changes : https://learn.microsoft.com/en-us/dotnet/core/compatibility/8.0

C# 12

Evidemment, on ne peut pas parler de .NET 8 sans parler de C# 12. Généralement, Microsoft profite de la sortie d’une nouvelle version de son framework pour livrer une nouvelle version du langage C#.

Si vous souhaitez revoir le replay, il est disponible ici.

Pour ma part, j’ai noté trois nouveautés intéressantes :

Primary constructors

Cette fonctionnalité permet d’initialiser les champs d’une classe de manière plus directe. Dans le cas de constructeur simple, cela simplifie grandement la lecture et la maintenabilité du code (les DTO par exemple).

Voici un exemple :

// Avant :
public class Book
{
    private string _title;
    private string _author;
    private int _yearPublished;

    public Book(string title, string author, int yearPublished)
    {
        _title = title;
        _author = author;
        _yearPublished = yearPublished;
    }
}

// Après :
public class Book(string title, string author, int yearPublished) 
{
    public bool IsVintage => DateTime.Now.Year - yearPublished > 20;

    public override string ToString()
    {
        return $"This book, {title}, was written by {author} in {yearPublished}";
    }
}

Pour l’initialisation des propriétés, on note qu’il n’est plus nécessaire de créer explicitement des propriétés ni d’écrire un constructeur.

Les paramètres sont accessibles directement dans le scope de la classe (en tant que propriété privée). On peut donc les utiliser dans des fonctions ou des expressions.

// Constructor overload
public class Book(string title, string author, int yearPublished) 
{ 
    public Book(): this("My title", "My author", 2024) { }
    public Book(string title, string author): this(title, author, DateTime.Now.Year) { }
}

On peut toujours ajouter des constructeurs, à la seule condition d’appeler le primary constructor via le mot clé this.

public class ExampleController(IService myService)
{
    [HttpGet]
    public ActionResult<Something> Get()
    {
        return myService.GetSomething();
    }
}

Du côté de l’injection de dépendances, on y gagne aussi en ayant plus besoin de définir une propriété privée explicitement !

On se rapproche de la syntaxe des records introduit dans C# 9. Mais attention, quand les primary constructors génèrent une propriété privée, accessible uniquement dans la classe et modifiable, les records génèrent une propriété avec get; init;, qui permet donc la lecture en dehors de la classe mais sans modification possible après son initialisation.

L’attribut Experimental

Au même titre que l’attribut Obsolete, on peut désormais marquer un type, une méthode ou une assembly avec l’attribut Experimental.

Comme son nom l’indique, cela permet d’indiquer qu’il s’agit d’une fonctionnalité expérimentale, et que le code est susceptible de changer à l’avenir.

A la compilation, un avertissement est généré.

La nouvelle syntaxe pour les collections

Dans la même démarche que pour les constructeurs, C# 12 apporte plusieurs nouveautés côté syntaxe pour les collections. L’idée est encore une fois d’obtenir quelque chose de plus concis et d’optimiser la lecture.

Commençons par regarder la simplification lors de la déclaration de certains types de collection :

// --- Array 
// Avant :
int[] a = new int[] { 1, 2, 3, 4, 5 }
var a = new int[] { 1, 2, 3, 4, 5 }
// Après : 
int[] a = [1, 2, 3, 4, 5];

// --- List
// Avant : 
List<string> b = new List<string> { "one", "two", "three" }
var b = new List<string> { "one", "two", "three" }
// Après : 
List<string> b = ["one", "two", "three"];

// --- Span
// Avant : 
var array = new char[] { 'a', 'b', 'c', 'd', 'e' }
Span<char> c = array.AsSpan()
var c = array.AsSpan()
// Après : 
Span<char> c  = ['a', 'b', 'c', 'd', 'e'];

A noter tout de même, l’utilisation de cette nouvelle syntaxe offre parfois des performances inférieures à l’ancienne méthode. Je vous laisse jeter un oeil à cet article.

Attention, avec ces nouvelles syntaxes, il n’est plus possible d’utiliser le mot clé var. Il faut préciser le type explicitement.

Autre nouveauté, directement inspiré du Javascript, l’opérateur de propagation (spread operator). Il permet de combiner facilement plusieurs collections.

int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[] single = [..row0, ..row1, ..row2];
foreach (var element in single)
{
    Console.Write($"{element}, ");
}
// output:
// 1, 2, 3, 4, 5, 6, 7, 8, 9,

Attention, contrairement au Javascript où il y a 3 points (), la syntaxe C# 12 n’en comporte que 2 (..) !

Entity Framework Core 8

Qui dit nouvelle version du framework, dit souvent nouvelle version de son ORM phare : Entity Framework !

Les nouveautés sont nombreuses, et je ne vais pas toutes les présenter. Si vous souhaitez tout découvrir, je vous invite à consulter ce lien.

On notera des améliorations concernant la prise en charge du JSON dans une base de données, qui a le vent en poupe ces dernières années. Globalement, EF fait des efforts sur la lisibilité des requêtes générées, notamment via la suppression des parenthèses inutiles. Et évidemment, toute une série d’optimisation côté performance, comme par exemple l’utilisation de IN SQL à la place du EXISTS lors de l’utilisation d’opérateur LINQ .Contains(...) dans une sous-requête.

Je souhaite faire le focus sur deux fonctionnalités :

Objets valeur utilisant des types complexes

Il existe trois grandes catégories d’objets que l’on peut enregistrer dans une base de données :

  • les objets non structurés mono-valeur (int, Guid, string…), appelés aussi types primitifs
  • les objets structurés pluri-valeurs dont l’identité est définie par une clé (Blog, Post, Customer), appelés aussi types d’entrées
  • les objets structurés pluri-valeurs mais sans clé définissant l’identité (Address, Coordinate)

C’est ce troisième type qui est concerné par cette nouveauté d’EF8.

Jusqu’à maintenant, la solution préconisée était de passer par les owned entities. Mais en réalité, cette solution impliquait une clé primaire cachée pour le suivi des objets par Entity Framework.

EF8 introduit donc la notion de types complexes. Ce type n’est pas identifié ni suivi par une clé et doit être défini uniquement dans le cadre d’un type d’entité (on ne peut donc pas avoir de DbSet de type complexe).

Voyons un peu le code :

public class PhoneNumber
{
    public required int CountryCode { get; set; }
    public required long Number { get; set; }
}

public class Customer
{
    public Guid Id { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public required PhoneNumber { get; set; }
}

public class Company 
{
    public Guid Id { get; set; }
    public required string Name { get; set; }
    public required PhoneNumber { get; set; }
    public required FaxNumber { get; set; }
}

Il est plutôt logique de regrouper les propriétés CountryCode et Number au sein d’une même entité car elles seront le plus souvent utilisées ensemble. De plus, l’encapsulation dans une même classe va permettre une réutilisation dans plusieurs entités. Dans le cas où l’on a besoin de faire évoluer l’entité (par exemple, ajouter une propriété type pour spécifier si c’est un fixe ou un mobile), les modifications seront appliquées dans l’ensemble des entités qui l’utilisent.

Côté configuration, c’est assez simple. On peut utiliser la syntaxe avec l’attribut [ComplexType] ou passer par une surcharge de OnModelCreating avec la méthode .ComplexProperty(...).

[ComplexType]
public class PhoneNumber
{ 
    ...
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .ComplexProperty(x => x.PhoneNumber);
    modelBuilder.Entity<Company>(c => 
    {
        c.ComplexProperty(x => x.PhoneNumber);
        c.ComplexProperty(x => x.FaxNumber);
    });
}

Lors de la création d’une entité, voici comment ça se passe :

var customer = new Customer()
{
    LastName = "Anceret",
    FirstName = "Matthieu",
    PhoneNumber = new () 
    {
        Country = 33,
        Number = 601020304
    }
}
dbContext.Add(customer);
await dbContext.SaveChangesAsync();

// Traduction en SQL
INSERT INTO [Customers] ([LastName], [FirstName], [PhoneNumber_Country], [PhoneNumber_Number])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3);

Notez bien que EF ne créé pas de table dédié pour stocker le type complexe, elles sont enregistrées inline dans les colonnes de la table principale.

Attention, si vous utilisez la même instance de PhoneNumber à plusieurs emplacements (comme dans Company par exemple), et que vous modifiez la valeur d’une propriété, ce changement sera répercuté à l’ensemble des utilisations. Pour éviter cela, il faut rendre le type immuable. Il existe plusieurs solutions pour faire cela :

// Les classes immuables, c'est l'occasion de revoir les constructeurs primaires ;)
public class PhoneNumber(int countryCode, long number)
{
    public int CountryCode { get; } = countryCode;
    public long Number { get; } = number;
}

// Les records
public record PhoneNumber(int countryCode, long number);

Lors d’une requête, EF va traiter les types complexes comme une propriété standard (autrement dit, qui n’est pas une propriété de navigation). Cela veut dire qu’elles sont toujours chargées quand le type d’entité est chargé (c’est valable aussi en cas de type complexe imbriqué). On peut évidemment utiliser les projections et se servir des types complexes dans les prédicats.

var customer = await dbContext.Customers.FirstAsync(x => x.Id == customerId);

// Traduction en SQL
SELECT TOP(1)
    [c].[Id], [c].[LastName], [c].[FirstName], [c].[PhoneNumber_CountryCode], [c].[PhoneNumber_Number]
FROM [Customers] AS [c]
WHERE [c].[Id] = @__customerId_0

Cela ne se prête pas à tous les cas d’usage, mais c’est une évolution sympathique et pratique :)

HierarchyId

HierarchyId est un type de donnée spécifiques utilisé pour stocker des données hiérarchiques (formant une structure arborescente) : structure d’organisation, système de fichiers, arbre généalogique, graphe de liens…

La prise en charge non officielle dans EF Core existe depuis plusieurs années via le package EFCore.SqlServer.HierarchyId. Désormais, c’est directement intégré dans EF Core via Microsoft.EntityFrameworkCore.SqlServer.hierarchyId.

Pour en savoir plus sur son utilisation, je vous redirige vers le sample de Microsoft.

.NET MAUI

En préambule, et pour rappel, .NET MAUI est le framework de Microsoft permettant de développer des applications cross-platform en full C#. C’est une alternative à React Native ou Flutter.

Ces derniers mois, .NET MAUI a beaucoup gagné en popularité, et ça se ressent notamment sur l’activité GitHub du projet. Microsoft annonce avoir fermé plus de 1600 PRs (pull requests) et près de 600 issues !

Après plus de 18 mois d’existence, de plus en plus d’entreprises font confiance à MAUI (3M, UPS, NBC Sports NEXT…). Et Microsoft n’est pas en reste, avec les applications Microsoft 365 Admin et Microsoft Azure, ainsi que l’application de point de vente pour Microsoft Dynamics 365 Commerce : Store Commerce.

Du côté des nouveautés introduites avec .NET 8, on note un effort important de Microsoft sur les performances et la gestion de la mémoire, ainsi que sur la réduction de la taille des applications.

Cela est valable aussi bien pour les applications full C#/XAML que celles hybrides Blazor + .NET MAUI. Cette dernière approche est d’ailleurs très intéressante, notamment pour transformer un site web en application desktop. s Il existe une quantité impressionnante d’autres nouveautés, aussi bien côté UX/UI (drag&drop, gestion du pointeur, contrôle visuel…) que techniques (géolocalisation, capteurs…). Je vous laisse consulter ce lien pour en découvrir l’exhaustivité.

Pour terminer, il existe désormais une extension pour VS Code, qui permet de créer des applications .NET MAUI.

Personnellement, je trouve que .NET MAUI a énormément gagné en maturité, et que c’est désormais une solution crédible pour faire du développement multi-plateformes. C’est particulièrement vrai si vous êtes dans un environnement full-Microsoft (équipe + stack technique) avec lequel l’efficacité est optimale.

Polly v8

J’ai déjà parlé de Polly dans un précédent article. Pour rappel, il s’agit d’une librairie .NET permettant d’aider à implémenter des patterns de résilience connus dans vos applications. Si vous ne l’utilisez pas déjà, je vous invite à le faire. Vos applications y gagneront !

La version 8 est une release majeure comportant de nombreuses améliorations et nouveautés, en particulier du côté des performances. Mais elle apporte son lot de breaking changes, qui nécessiteront un refactoring dans votre code.

Le premier changement est plutôt un élément de langage. On ne parle plus de policy mais de strategy. De ce changement, découle la notion de pipeline de résilience, qui est un outil permettant de combiner une ou plusieurs stratégies de résilience (anciennement Policy Wrap).

Il y a eu aussi pas mal de nettoyage côté code, notamment la suppression de toutes les APIs statiques, l’ajout de la gestion native de l’asynchronisme, une configuration plus souple et l’intégration de la télémétrie directement dans les stratégies.

Alors, concrètement, comment on fait ?

var pipeline = new ResiliencePipelineBuilder()
    .AddRetry(new RetryStrategyOptions(
        {
            MaxRetryAttempts = 5,
            Delay = TimeSpan.FromSeconds(1),
            BackoffType = DelayBackoffType.Exponential
        }
    )) 
    .AddTimeout(TimeSpan.FromSeconds(10)) 
    .Build();

var response = await pipeline.ExecuteAsync(
    async token => 
    { 
        return await httpClient.GetAsync(endpoint, token);
    }, 
    cancellationToken);

Microsoft a aussi sorti un package NuGet : Microsoft.Extensions.Http.Resilience. Celui-ci se base sur Polly pour proposer des pipelines pré-définis spécifiquement dédié au HttpClient.

services.AddHttpClient("my-http-client").AddStandardResilienceHandler();

En faisant ça, on va automatiquement ajouter 5 stratégies dans l’ordre suivant :

  • Rate limiter
  • Total request timeout
  • Retry
  • Circuit breaker
  • Attempt timeout

Si vous souhaitez migrer une application de la v7 vers la v8, Polly a publié un guide indiquant la marche à suivre.

Conclusion

Pour conclure, la .NET Conf a été une vitrine fascinante des avancées et des nouveautés dans l’univers .NET. Des améliorations significatives ont été apportées à .NET 8 et Entity Framework 8. Le langage phare, C#, évolue en version 12 pour toujours plus de confort et de simplicité pour les développeurs. Enfin, .NET MAUI affirme le retour de Microsoft dans le monde du mobile et du développement cross-platform.

Toutefois, le paysage de .NET s’étend bien au-delà de ces sujets. Des initiatives passionnantes comme .NET love AI et Semantic Kernel montrent les ambitions de Microsoft dans le domaine de l’intelligence artificielle. A noter aussi Project Kiota, qui est un outil permettant de générer un client d’API dans plusieurs langages (C#, Go, Java, Python, Typescript…) à partir d’une spécification OpenAPI.

Dans les semaines à venir, j’aborderais plus en détails .NET Aspire mais aussi les améliorations apportées à la conteneurisation des applicatifs .NET ainsi qu’à Native AOT. Stay tuned !