Accueil › Nouveautés › Chapitre 21

Tout .NET 10 LTS

Sorti en novembre 2025, supporté jusqu'en novembre 2028. C'est la version sur laquelle bâtir aujourd'hui, et celle qu'utilisent tous les exemples de cette documentation. Ce chapitre est un inventaire orienté « qu'est-ce que ça change pour moi », pas une liste de notes de version.

Le résumé en dix lignes

Migrer depuis .NET 8 : la liste de contrôle

# 1. Le TFM
#    <TargetFramework>net8.0</TargetFramework> → net10.0
# 2. Les paquets Microsoft.* alignés sur 10.x
dotnet list package --outdated
# 3. Reconstruire en traitant les avertissements comme des erreurs
dotnet build -warnaserror
# 4. Passer les tests, puis lire les changements de comportement annoncés
dotnet test
À vérifier après migrationPourquoi
Requêtes 401/403 sur les endpoints d'APIUne requête non authentifiée vers un endpoint d'API renvoie désormais 401/403 au lieu d'une redirection vers la page de connexion. Plus correct — mais si ton front interprétait la redirection, il casse
Documents OpenAPI générésVersion 3.1 par défaut : les types nullables s'écrivent type: [null, "string"]. Certains générateurs de clients anciens ne comprennent pas
Colonnes JSON EF Core sur SQL Server 2025Les colonnes nvarchar contenant du JSON peuvent basculer vers le type json à la première migration. Vérifie la migration générée
Requêtes avec collections paramétréesNouvelle stratégie de traduction par défaut (un paramètre par valeur) : plans d'exécution différents, généralement meilleurs
Journalisation EFLes constantes intégrées au SQL sont désormais masquées par défaut dans les logs (bonne nouvelle, mais tes logs changent)

Dans la pratique, une migration .NET 8 → 10 sur une application web bien testée se règle en une demi-journée. Le gros du travail est de lire les changements de comportement documentés, pas de réécrire du code.

C# 14

Membres d'extension

public static class ExtensionsProduit
{
    extension(Produit produit)
    {
        // PROPRIÉTÉ d'extension : impossible avant C# 14
        public bool EstEnPromotion => produit.PrixBarre is not null;
        public decimal Economie => (produit.PrixBarre ?? produit.Prix) - produit.Prix;

        // Méthode, comme avant
        public string Etiquette() => produit.EstEnPromotion
            ? $"{produit.Nom} (-{produit.Economie:C})"
            : produit.Nom;
    }

    extension(Produit)          // membres STATIQUES d'extension
    {
        public static Produit Vide => new() { Nom = "(aucun)", Prix = 0 };
        public static Produit Depuis(string csv) => /* ... */ null!;
    }
}

// Utilisation : indistinguable d'un vrai membre du type
if (produit.EstEnPromotion) Console.WriteLine(produit.Etiquette());
var p = Produit.Depuis(ligne);

Utile en particulier sur les types que tu ne possèdes pas (types du framework, code généré, bibliothèque tierce), là où l'héritage n'est pas une option.

Le mot-clé field

// Avant : déclarer, nommer et gérer le champ de soutien à la main
private string _reference = "";
public string Reference
{
    get => _reference;
    set => _reference = value.ToUpperInvariant();
}

// C# 14
public string Reference
{
    get;
    set => field = value.ToUpperInvariant();     // « field » = le champ généré
}

// Autre usage fréquent : initialisation paresseuse
public List<Ligne> Lignes => field ??= Charger();
Les autres apports de C# 14
// Affectation conditionnelle au null : n'affecte que si la cible existe
commande?.Adresse = nouvelleAdresse;
utilisateur?.Preferences?.Theme = "sombre";

// nameof sur un générique non lié
var nom = nameof(Dictionary<,>);          // "Dictionary"

// Modificateurs sur les paramètres de lambda (sans répéter les types)
var tryParse = (string s, out int r) => int.TryParse(s, out r);

// Conversions implicites vers Span : moins de surcharges à écrire
void Traiter(ReadOnlySpan<byte> donnees) { }
Traiter(monTableau);                       // conversion directe

// Constructeurs et événements partiels (utile aux générateurs de source)
public partial class Vue { public partial Vue(IService s); }

// Opérateurs d'affectation composée définis par l'utilisateur : += sans allocation
public void operator +=(Vecteur autre) { X += autre.X; Y += autre.Y; }

Runtime et performance

AméliorationEffet concret
Dévirtualisation étendue (tableaux, méthodes d'interface, structs)Les appels via interface ou foreach sur tableau deviennent des appels directs : gains « gratuits » sur du code existant
Allocation sur la pile de petits tableaux et de valeurs encapsuléesMoins de pression sur le ramasse-miettes, sans changer une ligne de code
Promotion de champs de structs, meilleure élimination de bornesBoucles numériques plus rapides
Prise en charge de jeux d'instructions récents (AVX 10.2)Accélération du traitement vectoriel sur processeurs modernes
DATAS (adaptation dynamique du tas) matureEmpreinte mémoire nettement réduite en conteneur, à débit comparable
Native AOT étenduPlus de scénarios compilables en natif, binaires plus petits
La bonne nouvelle

Ces gains ne demandent aucune modification de code : recompiler pour net10.0 suffit. Sur des applications web réelles, on observe couramment quelques pourcents de débit en plus et une empreinte mémoire en baisse — précisément pourquoi une montée de version majeure est presque toujours rentable.

Bibliothèques

// LINQ : jointures externes de première classe (enfin)
var lignes = etudiants.LeftJoin(
    departements,
    e => e.DepartementId,
    d => d.Id,
    (e, d) => new { e.Nom, Departement = d?.Nom ?? "(aucun)" });
// RightJoin existe aussi — et EF Core 10 les traduit en SQL

// LINQ asynchrone intégré (plus de paquet System.Linq.Async)
var total = await flux.Where(x => x.Actif).SumAsync(x => x.Montant, ct);
var liste = await flux.Take(100).ToListAsync(ct);

// Comparaison de chaînes avec ordre numérique : "fichier2" < "fichier10"
var comparateur = StringComparer.Create(CultureInfo.CurrentCulture,
                                        CompareOptions.NumericOrdering);
noms.Sort(comparateur);

// System.Text.Json : options plus fines
var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
    AllowOutOfOrderMetadataProperties = true,      // JSON polymorphe plus tolérant
    RespectNullableAnnotations = true              // les annotations nullables sont honorées
};

// Cryptographie post-quantique (selon la plateforme) : ML-KEM, ML-DSA, SLH-DSA
// Utile pour les systèmes à longue durée de conservation des données chiffrées.

ASP.NET Core 10

Validation intégrée aux Minimal API

builder.Services.AddValidation();

public record CreerClient(
    [Required, StringLength(80, MinimumLength = 2)] string Nom,
    [Required, EmailAddress]                        string Email,
    [Range(18, 130)]                                int Age);

app.MapPost("/clients", (CreerClient dto) => TypedResults.Created("/clients/1", dto));
// Invalide → 400 + ProblemDetails par champ, sans une ligne de code de validation.
// Traverse les objets imbriqués et les collections ; annoter [ValidatableType] au besoin.
// Désactivation ponctuelle : .DisableValidation()

Les API de validation vivent désormais dans Microsoft.Extensions.Validation, donc utilisables hors HTTP (services de fond, traitements par lots).

Server-Sent Events

app.MapGet("/pouls", (CancellationToken ct) =>
{
    async IAsyncEnumerable<Mesure> Flux([EnumeratorCancellation] CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            yield return new Mesure(Random.Shared.Next(60, 100), DateTime.UtcNow);
            await Task.Delay(2000, ct);
        }
    }
    return TypedResults.ServerSentEvents(Flux(ct), eventType: "pouls");
});

OpenAPI

Blazor

// Fin de la double initialisation au prérendu : un attribut suffit
[PersistentState]
public List<Film>? Films { get; set; }

protected override async Task OnInitializedAsync()
    => Films ??= await Service.ChargerAsync();
Nouveauté Blazor 10À quoi ça sert
[PersistentState]Transporter l'état du prérendu vers la phase interactive, sans enregistrement manuel
NotFoundPage sur le routeur + NavigationManager.NotFound()Une vraie page 404 pilotée depuis le code
InputHidden, validation par générateur de sourceFormulaires plus complets, validation des objets imbriqués et collections
Passkeys (WebAuthn) dans le modèle Blazor Web AppConnexion sans mot de passe, résistante au hameçonnage
Interop JS : InvokeConstructorAsync, GetValueAsync, SetValueAsyncManipuler des objets JavaScript sans écrire de fonction d'aide
Script Blazor servi comme actif statique, préchargement des ressourcesCompression et empreintes automatiques : premier rendu plus rapide
QuickGrid : RowClass, fermeture programmée des options de colonneMise en forme conditionnelle des lignes
Persistance et reconnexion de circuit améliorées, ReconnectModalUne coupure réseau ne détruit plus la session utilisateur
Hot Reload pour WebAssemblyBoucle de développement enfin comparable au mode serveur

Sécurité et divers

EF Core 10

Types complexes : la bonne façon de modéliser une valeur imbriquée
// Table splitting : les colonnes vivent dans la table du propriétaire
mb.Entity<Client>(b =>
{
    b.ComplexProperty(c => c.AdresseLivraison);      // Adresse_Rue, Adresse_Ville…
    b.ComplexProperty(c => c.AdresseFacturation);    // optionnel accepté depuis EF 10
});

// Ou une seule colonne JSON, collections comprises
mb.Entity<Client>().ComplexProperty(c => c.Preferences, p => p.ToJson());

// Sémantique de VALEUR : ce qui échouait avec les owned types fonctionne
client.AdresseFacturation = client.AdresseLivraison;    // simple copie de propriétés
await db.SaveChangesAsync();

// ExecuteUpdate fonctionne aussi dans le JSON
await db.Blogs.ExecuteUpdateAsync(s =>
    s.SetProperty(b => b.Details.Vues, b => b.Details.Vues + 1), ct);
Filtres de requête nommés : plusieurs filtres, désactivables un par un
mb.Entity<Blog>()
  .HasQueryFilter("SuppressionLogique", b => !b.EstSupprime)
  .HasQueryFilter("Locataire", b => b.LocataireId == _locataire.Id);

var corbeille = await db.Blogs.IgnoreQueryFilters(["SuppressionLogique"]).ToListAsync(ct);
ExecuteUpdate avec un lambda ordinaire (fin des arbres d'expression à la main)
await db.Blogs.ExecuteUpdateAsync(s =>
{
    s.SetProperty(b => b.Vues, 8);
    if (nomChange) s.SetProperty(b => b.Nom, "nouveau");   // conditionnel : impossible avant
}, ct);
Autre nouveauté EF 10Intérêt
Recherche vectorielle SQL Server (SqlVector<float>, EF.Functions.VectorDistance)Recherche sémantique et RAG directement en base, sans base vectorielle dédiée
Type json natif (SQL Server 2025 / Azure SQL)Stockage et requêtes JSON bien plus efficaces qu'en nvarchar
Recherche plein texte et recherche hybride sur Cosmos DBPertinence textuelle + similarité vectorielle combinées
Traduction de LeftJoin / RightJoinFin du triptyque SelectMany/GroupJoin/DefaultIfEmpty
Collections paramétrées : un paramètre SQL par valeur (avec remplissage)Meilleurs plans d'exécution et cache de plans préservé
Constantes intégrées masquées dans les logsMoins de données personnelles qui fuient dans les journaux
Analyseur contre la concaténation dans FromSqlRawDétection d'une injection SQL potentielle à la compilation
Ordre cohérent dans les requêtes découpéesCorrige une source de corruption de données silencieuse
Noms de contraintes par défaut personnalisablesConventions de nommage d'entreprise respectées

SDK et outillage

# Exécuter un simple fichier .cs, sans projet : idéal pour un script ou une démonstration
cat > bonjour.cs <<'EOF'
#:package Humanizer@2.14.1
Console.WriteLine(TimeSpan.FromMinutes(135).Humanize());
EOF
dotnet run bonjour.cs

# Et le convertir en vrai projet quand il grandit
dotnet project convert bonjour.cs

# Verbes CLI réorganisés (les anciennes formes fonctionnent toujours)
dotnet package add Serilog
dotnet package list --vulnerable
dotnet package search json

# Exécuter un outil sans l'installer durablement
dnx dotnet-ef -- migrations list

Côté conteneurs, dotnet publish /t:PublishContainer gagne des options (dont les images chiseled, sans shell), et les modèles de tests basculent sur xUnit v3 avec Microsoft.Testing.Platform.

Vérifie que c'est passé

🎯 Quiz — 4 questions

1. Après migration en .NET 10, ton front JavaScript reçoit du HTML au lieu d'un JSON d'erreur sur une requête non authentifiée. Que s'est-il passé ?

Le nouveau comportement est le bon. Si ton client s'appuyait sur la redirection, adapte-le — ou rétablis l'ancien comportement via OnRedirectToLogin.

2. Quel est l'apport principal des membres d'extension de C# 14 ?

Les méthodes d'extension existaient depuis 2007 ; C# 14 complète le mécanisme (propriétés, statiques, indexeurs en C# 15) dans un bloc extension.

3. Pourquoi préférer un type complexe EF 10 à un owned type pour une adresse ?

Un owned type reste une entité avec une identité cachée : d'où l'échec de client.Facturation = client.Livraison et l'absence de support d'ExecuteUpdate.

4. Que faut-il faire pour bénéficier des gains de performance du runtime .NET 10 ?

Dévirtualisation, allocations sur la pile, DATAS : tout cela est transparent. C'est ce qui rend une montée de version presque toujours rentable.
À retenir