Préambule
Microsoft prévoit de lancer .NET 11 le 10 novembre 2026. Ce article s’appuie sur la Release Candidate 2, publiée le 13 octobre 2026. Elle bénéficie d’un support « go-live » permettant son utilisation en production avec le support de Microsoft. Certaines fonctionnalités, comme Runtime Async et les nouvelles règles de sécurité mémoire, conservent toutefois leur propre statut de préversion. Il faut donc regarder le statut de chaque nouveauté, même à cette étape du cycle.
L’an dernier, dans mon article sur .NET 10, je présentais une version qui combinait performances, évolution du langage et support à long terme. .NET 11 poursuit plusieurs de ces travaux, avec des changements particulièrement intéressants pour les applications métier.
Les types union et les hiérarchies fermées de C# 15 permettent de mieux exprimer les résultats et les états possibles. Runtime Async fait évoluer l’exécution du code asynchrone. Les bibliothèques, ASP.NET Core, EF Core et Blazor continuent aussi de réduire le code nécessaire pour des besoins courants.
Ce panorama se concentre sur le backend, les API, les données et Blazor. Une place plus importante est accordée à C# 15, avec un fil conducteur autour de la gestion de dossiers. L’objectif est de comprendre ce que ces nouveautés apportent à nos applications et les décisions qu’elles impliquent.
Des performances toujours en progression
Le JIT réduit le coût de certaines abstractions
Les évolutions du runtime .NET 11 comprennent de nouvelles optimisations du JIT, notamment pour la dévirtualisation, l’élimination de vérifications de bornes et le calcul anticipé d’expressions constantes.
Prenons un service qui appelle une règle métier à travers une interface. Cet appel permet de substituer différentes implémentations, mais ajoute une indirection. Lorsque le JIT possède suffisamment d’information sur le type réellement utilisé, il peut transformer cet appel en un appel direct. C’est la dévirtualisation.
Il peut ensuite intégrer le corps de la méthode dans la méthode appelante, ce qu’on appelle l’inlining. Cette visibilité supplémentaire permet d’éliminer des calculs ou des branches devenus inutiles.
Pour une application métier, l’intérêt est de conserver des abstractions utiles tout en réduisant certains de leurs coûts. Je continuerais donc à choisir les interfaces selon les besoins de conception. Les supprimer préventivement pour « aider les performances » reste une mauvaise justification sans mesure du chemin concerné.
Des résultats concrets, à replacer dans leur scénario
Comme chaque année, Stephen Toub détaille une grande quantité d’optimisations dans Performance Improvements in .NET 11. Ses microbenchmarks, généralement réalisés avec BenchmarkDotNet, permettent d’observer précisément les opérations qui progressent.
Voici deux résultats de son article. Ce sont ses mesures, pas des benchmarks exécutés pour ce billet.
| Scénario mesuré par Stephen Toub | .NET 10 | .NET 11 | Allocation mesurée |
|---|---|---|---|
Comparer un ImmutableArray<int> à une List<int> de 1 000 éléments avec SequenceEqual | 925,8 ns | 122,2 ns | Non présentée dans ce tableau de résultats |
Appeler Array.FindAll sur quatre entiers qui satisfont tous le prédicat | 27,61 ns | 9,212 ns | 112 octets → 40 octets |
Dans le premier cas, la comparaison exploite mieux les collections connues. Dans le second, le traitement des petits résultats évite du stockage temporaire inutile.
Ces résultats montrent où le travail disparaît. Ils ne prédisent pas l’accélération d’une API complète. Une requête qui passe l’essentiel de son temps à attendre SQL Server restera principalement limitée par cet accès aux données.
Pour évaluer une migration, je mesurerais les latences aux percentiles 95 et 99, le débit, les allocations et la consommation CPU sur des parcours représentatifs. Un microbenchmark explique un mécanisme. Un essai de charge indique ce qu’il apporte au système.
Runtime Async : faire évoluer l’exécution de async et await
Traditionnellement, le compilateur C# transforme une méthode asynchrone en machine à états. Celle-ci conserve les informations nécessaires pour suspendre l’exécution, puis la reprendre après un await.
Avec Runtime Async, une partie de cette transformation passe au runtime. Le compilateur émet une représentation plus compacte et le moteur d’exécution dispose d’une meilleure visibilité sur les enchaînements asynchrones.
Runtime Async reste en préversion dans la RC 2. Le code applicatif l’active explicitement, même si les bibliothèques du runtime l’utilisent déjà :
1
2
3
4
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<Features>$(Features);runtime-async=on</Features>
</PropertyGroup>
La syntaxe async/await reste la même. Pour un projet net11.0, cette activation n’exige pas à elle seule LangVersion=preview ni EnablePreviewFeatures.
Stephen Toub explique que certaines chaînes d’appels peuvent ainsi éviter des objets Task intermédiaires. Il précise aussi que l’objectif reste la compatibilité de comportement et que des scénarios moins performants subsistent. async void, les itérateurs asynchrones et certains types de retour personnalisés continuent de suivre le mécanisme traditionnel.
L’amélioration des piles d’appels concerne notamment l’observation pendant l’exécution. Il ne faut pas la confondre avec l’affichage des exceptions, qui bénéficiait déjà d’un traitement particulier.
Je testerais Runtime Async séparément de la migration générale. Une première mesure évalue le passage à .NET 11. Une seconde évalue son activation dans l’application. Cette séparation facilite l’analyse d’un gain ou d’une régression.
C# 15 : un langage plus expressif
Depuis la RC 1, C# 15 est la version sélectionnée par défaut pour les projets ciblant .NET 11. Les types union, les hiérarchies fermées, les arguments des expressions de collection, les indexeurs d’extension et les sauts avec étiquette font partie des fonctionnalités stabilisées. L’évolution de la sécurité mémoire conserve un statut distinct de préversion.
Les notes C# de la RC 2 ne signalent aucune fonctionnalité supplémentaire du langage. Les nouveautés présentées ci-dessous restent donc celles de C# 15, avec des améliorations de leur intégration dans les bibliothèques et ASP.NET Core.
Les types union rendent les résultats possibles explicites
Une opération de création peut produire plusieurs résultats métier. Le dossier peut être créé, son numéro peut déjà être utilisé ou une règle peut empêcher sa création.
On peut représenter cela avec un objet contenant un booléen de succès, un identifiant nullable et un message nullable. Il faut alors documenter les combinaisons autorisées. Que signifie un succès sans identifiant ? Peut-on avoir simultanément un identifiant et une erreur ?
Un type union permet de nommer directement les alternatives :
1
2
3
4
5
6
7
8
9
10
public sealed record DossierCree(Guid Identifiant);
public sealed record NumeroDejaUtilise(string Numero);
public sealed record CreationRefusee(string Motif);
public union ResultatCreationDossier(
DossierCree,
NumeroDejaUtilise,
CreationRefusee);
Chaque cas possède ses propres données. Les types n’ont pas besoin d’hériter d’une classe commune. Une conversion implicite permet d’affecter un cas à l’union :
1
2
ResultatCreationDossier resultat =
new DossierCree(Guid.NewGuid());
Dans mon modèle, DossierCree porte obligatoirement un identifiant et CreationRefusee porte un motif. Cela réduit les combinaisons de propriétés à interpréter. Les invariants à l’intérieur d’un cas, comme l’interdiction d’un motif vide, restent toutefois à implémenter.
Traiter les cas avec une expression switch
Le pattern matching des unions s’applique à la valeur contenue :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static class PresentationCreationDossier
{
public static string Formater(ResultatCreationDossier resultat) =>
resultat switch
{
DossierCree(var identifiant) =>
$"Le dossier {identifiant} a été créé.",
NumeroDejaUtilise(var numero) =>
$"Le numéro {numero} est déjà utilisé.",
CreationRefusee(var motif) =>
$"La création a été refusée : {motif}",
null =>
throw new InvalidOperationException(
"Le résultat de création n’a pas été initialisé.")
};
}
Le cas null mérite une explication. La déclaration union génère une structure. Sa valeur default peut donc exister, avec une valeur interne nulle, même lorsque les cas déclarés ne sont pas nullables. Ici, ce cas signale une erreur de programmation. Ce n’est pas un quatrième résultat métier.
Lorsqu’un cas métier manque dans une expression switch, le compilateur produit un avertissement d’exhaustivité. Il ne s’agit pas automatiquement d’une erreur de compilation. Une équipe qui veut en faire une barrière de livraison doit configurer ses avertissements en conséquence.
J’éviterais aussi d’ajouter systématiquement un bras _ => .... Il capture les nouveaux cas et retire précisément le signal recherché lorsqu’on fait évoluer le contrat. Un traitement explicite de null ne remplace pas la couverture des types métier.
Ce que cela change dans la conception
Supposons que l’on ajoute ultérieurement QuotaDossiersAtteint. Les consommateurs recompilés qui traitent les cas explicitement pourront être signalés. Le développeur doit alors décider comment présenter, journaliser ou traduire ce nouveau résultat.
Je trouve cette approche particulièrement intéressante pour les commandes applicatives. Elle force à nommer ce qui peut arriver. Un « échec » générique devient par exemple un conflit de numéro ou un refus métier, avec les données nécessaires pour réagir.
Je conserverais les exceptions pour les situations qui ne font pas partie du résultat métier attendu, selon les conventions de l’application. Une indisponibilité inattendue du stockage n’a pas besoin de devenir un cas dans chaque union.
Des limites à connaître
La spécification des unions décrit aussi leur représentation. La forme générée stocke son contenu dans une référence object?. Les cas de type valeur, comme int ou une structure métier, subissent donc du boxing. Une union n’est pas une garantie de zéro allocation.
De plus, ajouter un cas à une API publique demande une réflexion sur la compatibilité. Les avertissements aident les consommateurs qui recompilent. Ils ne mettent pas à jour les applications déjà déployées.
Enfin, je réserverais une union spécialisée aux opérations dont les différences entre résultats ont une valeur réelle. Un Result<T> existant peut déjà couvrir correctement une grande partie des besoins. La nouveauté du langage ne justifie pas à elle seule une réécriture généralisée.
Les hiérarchies fermées encadrent l’héritage
Une union regroupe des types. Une hiérarchie fermée permet plutôt de conserver une relation d’héritage tout en encadrant les descendants directs.
Voici une représentation des états d’un dossier :
1
2
3
4
5
6
7
8
9
public closed record EtatDossier;
public sealed record Brouillon : EtatDossier;
public sealed record EnAnalyse(DateOnly DateReception) : EtatDossier;
public sealed record Accepte(DateOnly DateDecision) : EtatDossier;
public sealed record Refuse(string Motif) : EtatDossier;
Le type EtatDossier est implicitement abstrait. Ses descendants directs doivent être déclarés dans le même assembly et le même module. Le compilateur peut alors raisonner sur cet ensemble connu :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static class PresentationEtatDossier
{
public static string Decrire(EtatDossier etat) =>
etat switch
{
Brouillon =>
"Le dossier est en préparation.",
EnAnalyse(var dateReception) =>
$"Le dossier est en analyse depuis le {dateReception:yyyy-MM-dd}.",
Accepte(var dateDecision) =>
$"Le dossier a été accepté le {dateDecision:yyyy-MM-dd}.",
Refuse(var motif) =>
$"Le dossier a été refusé : {motif}"
};
}
La fermeture n’est pas transitive. Un descendant ordinaire peut encore être étendu dans un autre assembly. Dans cet exemple, sealed ferme les feuilles. Un descendant intermédiaire pourrait plutôt être closed pour définir un autre ensemble de sous-types.
Cette représentation permet d’associer des données à chaque état. Elle ne définit toutefois pas les transitions autorisées. Le fait que Accepte et Refuse existent ne dit pas si l’on peut passer directement de l’un à l’autre. Cette règle appartient toujours au comportement métier.
Union, hiérarchie fermée ou énumération ?
Voici la grille que j’utiliserais pour notre exemple :
| Besoin | Choix envisagé | Exemple |
|---|---|---|
| Représenter quelques valeurs sans données propres à chaque cas | enum | Une priorité faible, normale ou élevée |
| Composer des résultats de types indépendants | union | Un dossier créé, un numéro déjà utilisé ou un refus |
| Représenter une famille de types qui partagent une base | Hiérarchie closed | Les états d’un dossier et leurs données |
| Permettre à d’autres modules d’ajouter des comportements | Interface ou hiérarchie ouverte | Des règles fournies par des extensions |
Pour les états, mon choix dépendrait notamment de la quantité de données et de comportements associés. Si une simple énumération décrit correctement le besoin, je la conserverais.
Configurer une collection dans son expression
Les expressions de collection deviennent plus pratiques lorsqu’on doit préciser un comparateur ou une capacité. C# 15 ajoute un élément with(...) placé au début de l’expression.
1
2
3
4
5
6
7
8
9
10
11
12
HashSet<string> codesAutorises =
[
with(StringComparer.OrdinalIgnoreCase),
"OUVERT",
"ouvert",
"FERME"
];
Console.WriteLine(codesAutorises.Count); // 2
Console.WriteLine(codesAutorises.Contains("Ouvert")
? "Code autorisé."
: "Code refusé.");
Le comparateur participe à la construction de la collection. Les deux variantes de OUVERT représentent donc le même élément dès l’initialisation.
On peut aussi prévoir la capacité d’une liste :
1
2
3
4
5
6
7
string[] numerosExistants = ["DOS-001", "DOS-002"];
List<string> numeros =
[
with(capacity: 100),
.. numerosExistants
];
La capacité ne crée pas cent éléments. Elle réserve de l’espace pour des ajouts éventuels. Le nom capacity reste celui du paramètre du framework.
Les règles de résolution acheminent ces arguments au constructeur ou à la méthode de création appropriée. Ce mécanisme ne s’applique pas aux tableaux ni aux spans. Le type cible continue de déterminer les possibilités.
Les indexeurs d’extension : une syntaxe à employer avec discernement
Les blocs extension, introduits avec C# 14, peuvent maintenant déclarer des indexeurs d’extension.
Par exemple, une convention applicative peut donner accès à des valeurs d’en-tête à partir d’une énumération :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum EnteteApplicatif
{
Correlation,
VersionContrat
}
public static class ExtensionsEntetes
{
extension(IReadOnlyDictionary<string, string> entetes)
{
public string this[EnteteApplicatif entete] =>
entetes[entete switch
{
EnteteApplicatif.Correlation =>
"X-Correlation-Id",
EnteteApplicatif.VersionContrat =>
"X-Version-Contrat",
_ =>
throw new ArgumentOutOfRangeException(nameof(entete))
}];
}
}
L’appel devient :
1
2
3
4
5
6
7
8
IReadOnlyDictionary<string, string> entetes =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["X-Correlation-Id"] = "correlation-123",
["X-Version-Contrat"] = "2"
};
string correlation = entetes[EnteteApplicatif.Correlation];
L’indexeur n’ajoute pas d’état au dictionnaire. Il fournit une autre forme d’accès. Ici, l’absence d’une clé conserve le comportement du dictionnaire sous-jacent.
La spécification des indexeurs d’extension précise leurs règles de résolution. Côté conception, je garderais cette syntaxe pour les accès qui se lisent naturellement comme une indexation. Une recherche coûteuse, un appel réseau ou une énumération complète méritent généralement une méthode dont le nom rend ce travail visible.
break et continue peuvent cibler une boucle nommée
Dans des parcours imbriqués, on ajoute parfois un indicateur pour quitter plusieurs boucles ou passer directement au prochain élément de la boucle extérieure.
C# 15 permet aux instructions break et continue de préciser une étiquette. Supposons que chaque lot contienne plusieurs numéros à valider :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
string[][] lotsNumeros =
[
["DOS-001", "DOS-002"],
["DOS-003", ""],
["DOS-004"]
];
lots: foreach (string[] numeros in lotsNumeros)
{
foreach (string numero in numeros)
{
if (string.IsNullOrWhiteSpace(numero))
{
Console.WriteLine("Le lot est ignoré : un numéro est manquant.");
continue lots;
}
}
Console.WriteLine($"Le lot contient {numeros.Length} numéros valides.");
}
continue lots passe au lot suivant. Un continue ordinaire ne passerait qu’au prochain numéro. De même, break lots quitterait la boucle extérieure.
L’étiquette désigne une construction englobante, conformément aux règles de cette fonctionnalité. Ce n’est pas un saut arbitraire vers n’importe quelle ligne.
Je l’utiliserais lorsque l’intention reste locale et évidente. Si la boucle contient plusieurs pages de règles, extraire une opération nommée peut encore être le meilleur choix.
Sécurité mémoire : une évolution encore en préversion
Le nouveau modèle de sécurité mémoire cherche à mieux distinguer la manipulation de pointeurs des opérations qui accèdent effectivement à la mémoire pointée.
Dans le modèle proposé, le modificateur unsafe d’un membre peut aussi transmettre une obligation à son appelant. La frontière à examiner devient donc visible dans le contrat du membre, au-delà de son implémentation.
Pour activer ces nouvelles règles avec le SDK .NET 11 RC 2 :
1
2
3
4
5
6
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Features>$(Features);updated-memory-safety-rules</Features>
</PropertyGroup>
La spécification d’évolution de unsafe introduit notamment unsafe(expression) pour limiter le contexte à une expression, ainsi qu’un modificateur safe pour certaines déclarations. Ce dernier exprime une décision de l’auteur. Il ne prouve pas automatiquement qu’un appel natif est sans risque.
Pour la majorité des API métier, les autres nouveautés de C# auront un effet plus direct. Les auteurs de bibliothèques d’interopérabilité ou de composants bas niveau ont toutefois intérêt à suivre ces travaux. Je traiterais leur adoption comme une évolution spécifique, avec une revue des frontières natives.
Les nouveautés des bibliothèques .NET
La validation asynchrone rejoint DataAnnotations
Vérifier un format se fait en mémoire. Vérifier la disponibilité d’un numéro peut nécessiter un accès à une base de données. Les contraintes ne sont pas les mêmes.
.NET 11 ajoute AsyncValidationAttribute, IAsyncValidatableObject et des méthodes de validation asynchrones. Une règle peut ainsi attendre une opération d’entrée-sortie sans bloquer un thread.
Voici un attribut qui utilise un service applicatif pour vérifier un numéro. L’implémentation de IRegistreDossiers doit être enregistrée dans le conteneur d’injection de dépendances.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
public interface IRegistreDossiers
{
Task<bool> EstNumeroExistant(
string numero,
CancellationToken cancellationToken);
}
public sealed class NumeroDossierDisponibleAttribute : AsyncValidationAttribute
{
protected override ValidationResult? IsValid(
object? valeur,
ValidationContext contexteValidation) =>
throw new InvalidOperationException(
"Cette règle doit être exécutée de façon asynchrone.");
protected override async Task<ValidationResult?> IsValidAsync(
object? valeur,
ValidationContext contexteValidation,
CancellationToken cancellationToken)
{
if (valeur is not string numero || string.IsNullOrWhiteSpace(numero))
{
return ValidationResult.Success;
}
var registre = contexteValidation.GetRequiredService<IRegistreDossiers>();
return await registre.EstNumeroExistant(numero, cancellationToken)
? new ValidationResult(
"Ce numéro de dossier est déjà utilisé.",
[contexteValidation.MemberName!])
: ValidationResult.Success;
}
}
public sealed class CreerDossierRequete
{
[Required(ErrorMessage = "Le numéro de dossier est obligatoire.")]
[NumeroDossierDisponible]
public string Numero { get; init; } = "";
}
La règle laisse à [Required] le traitement d’une valeur absente. Elle transmet aussi le jeton d’annulation au service. Le chemin synchrone échoue explicitement, puisqu’il ne peut pas réaliser la même vérification sans attendre une entrée-sortie.
Pour une validation déclenchée directement, Validator.ValidateObjectAsync reçoit l’objet et son contexte. Ici, requete, fournisseurServices et cancellationToken proviennent de l’opération courante :
1
2
3
4
5
6
7
8
9
10
var contexteValidation = new ValidationContext(
requete,
fournisseurServices,
items: null);
await Validator.ValidateObjectAsync(
requete,
contexteValidation,
validateAllProperties: true,
cancellationToken: cancellationToken);
Cette méthode lève une ValidationException lorsque l’objet est invalide. Les variantes de type TryValidate servent aux appels qui doivent récupérer les résultats de validation plutôt que suivre ce comportement.
Une vérification préalable d’unicité améliore le retour utilisateur, mais ne garantit pas l’unicité au moment de l’écriture. Deux requêtes peuvent vérifier le même numéro simultanément. Je conserverais une contrainte unique dans la base de données et une traduction appropriée du conflit lors de la sauvegarde.
Je reviendrai plus en détail sur la validation asynchrone dans un article dédié, qui suivra ce panorama de .NET 11. J’y approfondirai son utilisation avec DataAnnotations, son intégration dans les applications et les précautions à prendre lorsque les règles interrogent des services externes ou une base de données.
Des messages DataAnnotations plus faciles à localiser
La RC 2 ajoute ValidationAttribute.FormatMessage. Le code qui orchestre la validation peut fournir un modèle de message localisé, tandis que l’attribut fournit ses arguments, comme les bornes d’un intervalle.
1
2
3
4
5
6
7
8
9
10
using System.ComponentModel.DataAnnotations;
var regleDuree = new RangeAttribute(1, 30);
string message = regleDuree.FormatMessage(
"{0} doit être comprise entre {1} et {2} jours.",
"La durée de traitement");
Console.WriteLine(message);
// La durée de traitement doit être comprise entre 1 et 30 jours.
L’intérêt est de conserver le formatage des paramètres dans l’attribut. Un composant de localisation n’a plus à connaître lui-même les propriétés de chaque règle. Pour un attribut personnalisé qui possède ses propres paramètres de message, on peut redéfinir cette méthode.
JSON : unions et export de séquences
Les améliorations de System.Text.Json accompagnent les nouveaux types du langage. Le sérialiseur prend en charge les unions C# et peut exploiter les hiérarchies fermées pour configurer le polymorphisme.
La RC 2 corrige aussi plusieurs cas d’intégration, dont l’inférence des types dérivés dans des hiérarchies fermées imbriquées et le respect de JsonNumberHandlingAttribute pour les unions.
Il reste nécessaire de penser au contrat JSON. Deux cas qui produisent chacun un objet peuvent être ambigus à la désérialisation. Le compilateur connaît leurs types .NET, mais le client n’envoie pas nécessairement une information qui permet de les distinguer.
Pour un nouveau contrat public, je privilégierais une représentation explicite et stable. Une évolution interne du modèle ne devrait pas changer discrètement la façon dont les consommateurs identifient le type d’un message.
Autre ajout utile, SerializeAsyncEnumerable sait écrire une séquence asynchrone sous forme de JSON Lines :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Text.Json;
public sealed record DossierExporte(string Numero, string Etat);
public static class ExportDossiers
{
public static Task Ecrire(
Stream destination,
IAsyncEnumerable<DossierExporte> dossiers,
CancellationToken cancellationToken) =>
JsonSerializer.SerializeAsyncEnumerable(
destination,
dossiers,
topLevelValues: true,
cancellationToken: cancellationToken);
}
Chaque valeur occupe une ligne. Sans topLevelValues: true, la sortie est un tableau JSON. Ce format convient notamment aux exports et aux traitements progressifs, à condition que le destinataire le prenne en charge.
L’intérêt est de ne pas matérialiser toute la séquence avant de commencer sa sérialisation. Cela ne garantit pas que chaque élément sera immédiatement transmis au client, car les autres couches peuvent utiliser des tampons. Pour Native AOT, les surcharges recevant un JsonTypeInfo<T> permettent d’utiliser les métadonnées générées.
LINQ gagne une jointure externe complète
Après LeftJoin et RightJoin dans .NET 10, .NET 11 ajoute FullJoin. L’opérateur conserve les éléments des deux sources, même lorsqu’ils n’ont pas de correspondance.
Un rapprochement entre deux extractions devient plus lisible :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DossierExporte[] dossiersSource =
[
new("DOS-001", "Ouvert"),
new("DOS-002", "Fermé")
];
DossierExporte[] dossiersCible =
[
new("DOS-001", "En analyse"),
new("DOS-003", "Ouvert")
];
var rapprochements = dossiersSource.FullJoin(
dossiersCible,
source => source.Numero,
cible => cible.Numero,
(source, cible) => new
{
Numero = source?.Numero ?? cible!.Numero,
EtatSource = source?.Etat ?? "Absent",
EtatCible = cible?.Etat ?? "Absent"
});
Le résultat contient les trois numéros. DOS-002 manque dans la cible et DOS-003 manque dans la source. Le cas commun conserve les deux états pour permettre leur comparaison.
Dans cet exemple, les clés sont uniques dans chaque extraction. Une jointure ne déduplique pas automatiquement des clés répétées. Il faut en tenir compte avant de l’utiliser comme mécanisme de rapprochement.
Observer le cache mémoire
Parmi les ajouts aux bibliothèques .NET 11, MemoryCache expose des métriques compatibles avec OpenTelemetry. Leur activation passe notamment par TrackStatistics.
Pour une application d’entreprise, suivre les succès, les absences et les évictions du cache aide à vérifier qu’il apporte réellement le bénéfice attendu. Il faut ensuite collecter et exporter ces métriques avec l’outillage d’observabilité de l’application.
La RC 2 ajuste ces métriques. L’attribut dotnet.cache.request.type devient dotnet.cache.request.result. La métrique dotnet.cache.estimated_size utilise l’unité 1 pour une taille définie par l’application, pas nécessairement des octets. Les tableaux de bord préparés avec la RC 1 doivent tenir compte de ces changements.
Contrôler la concurrence initiale des connexions HTTP/2
La nouvelle propriété SocketsHttpHandler.InitialHttp2MaxConcurrentStreams permet de limiter le nombre de flux simultanés au démarrage d’une connexion HTTP/2, avant la réception des paramètres du serveur.
Pour un client nommé enregistré avec IHttpClientFactory, configuré pour demander HTTP/2 avec un repli possible vers HTTP/1.1 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Net;
var constructeur = WebApplication.CreateBuilder();
constructeur.Services
.AddHttpClient("RegistreDossiers", client =>
{
client.DefaultRequestVersion = HttpVersion.Version20;
client.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
InitialHttp2MaxConcurrentStreams = 20
});
Sans réglage ni limite déjà connue pour cet hôte, le client part d’une limite initiale de 100 flux. Réduire cette valeur peut être utile lorsqu’on sait qu’un service en accepte moins. Après la réception de la trame SETTINGS, la connexion utilise la limite annoncée par le serveur.
Je réserverais ce réglage aux clients pour lesquels le comportement initial pose un problème mesuré. Il s’applique par connexion et ne constitue pas un plafond global de concurrence pour l’application.
CBOR et cryptographie : des ajouts spécialisés
La RC 2 permet de lire du CBOR par blocs sans réunir tout le document dans un tampon contigu. Ce mode exige CborConformanceMode.Lax.
Elle ajoute aussi des API expérimentales CompositeMLKem et Hpke. Leur statut reste distinct du support « go-live » de la RC 2.
ASP.NET Core 11 et les API
Une validation asynchrone intégrée aux Minimal APIs
La validation des Minimal APIs peut maintenant attendre les règles asynchrones avant d’exécuter le traitement du point de terminaison. AddValidation() permet d’activer la validation automatique des paramètres pris en charge.
L’attribut NumeroDossierDisponible présenté précédemment peut donc être utilisé sur un modèle de requête. Le pipeline attend son résultat sans bloquer sur l’appel asynchrone.
Son emplacement influence toutefois le contrat HTTP. La validation automatique répond 400 lorsqu’un modèle est invalide. Pour que le numéro déjà utilisé produise le 409 proposé plus loin, je placerais cette vérification dans le service applicatif, avec un résultat métier explicite. L’attribut illustre le mécanisme de validation asynchrone, dont l’usage doit rester cohérent avec les conventions de l’API.
La documentation commune de validation précise cependant deux limites importantes. Cette intégration concerne les Minimal APIs et Blazor, pas automatiquement le pipeline de validation MVC ou Razor Pages. De plus, certaines validations peuvent s’exécuter en parallèle.
Ce dernier point influence la conception des services appelés. Plusieurs règles ne doivent pas lancer simultanément des requêtes sur la même instance de DbContext. Je privilégierais des services compatibles avec ce mode d’exécution, ou une validation applicative orchestrée lorsque les règles doivent être séquencées.
La localisation est également mieux intégrée. L’enregistrement d’un IStringLocalizerFactory par AddLocalization(), suivi de AddValidation(), active la résolution des messages localisés. Dans une application bilingue, cela permet de centraliser les textes sans les disperser dans les traitements HTTP.
Avec la RC 2, le pipeline utilise la méthode ValidationAttribute.FormatMessage présentée précédemment. L’interface IValidationMessageFormatter, introduite dans les préversions, a été retirée. Les notes de migration ASP.NET Core indiquent de redéfinir FormatMessage(string format, string name) pour les attributs personnalisés qui doivent fournir leurs propres arguments.
La RC 2 corrige également la validation des cas contenus dans les unions C#. Si un modèle d’entrée utilise une union, les règles de ses différents cas font donc partie des scénarios à couvrir dans les essais de l’API.
Mieux décrire les contrats avec OpenAPI
ASP.NET Core 11 ajoute la prise en charge d’OpenAPI 3.2. On peut choisir cette version explicitement :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using Microsoft.OpenApi;
var constructeur = WebApplication.CreateBuilder();
constructeur.Services.AddOpenApi(options =>
{
options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_2;
});
var application = constructeur.Build();
if (application.Environment.IsDevelopment())
{
application.MapOpenApi();
}
application.Run();
Cet extrait suppose une référence à une version 11 compatible de Microsoft.AspNetCore.OpenApi. Il montre uniquement la configuration du document.
La génération décrit aussi mieux certaines réponses binaires et différents schémas. Je vérifierais toutefois les consommateurs du document avant de changer sa version. Un générateur de client ou un outil de validation doit comprendre la spécification produite.
Pour nos résultats de création, le type C# ne décide pas à lui seul du code HTTP approprié. Je conserverais une traduction explicite à la frontière de l’API :
| Résultat métier | Réponse envisagée pour cet exemple |
|---|---|
DossierCree | 201 Created, avec l’identifiant et l’emplacement de la ressource |
NumeroDejaUtilise | 409 Conflict, avec un code d’erreur stable |
CreationRefusee | 422 Unprocessable Content, selon la convention retenue pour les refus métier |
Les unions peuvent traverser les points d’intégration ASP.NET Core, mais cela ne remplace pas la conception d’un contrat HTTP. Les modèles d’entrée, les réponses et les erreurs restent des décisions explicites de l’API.
HTTP QUERY : une meilleure description des recherches structurées
La méthode HTTP QUERY permet d’exprimer une recherche avec un corps de requête tout en conservant une sémantique sûre, sans modification métier demandée, et idempotente. Elle répond notamment aux cas où les critères deviennent peu pratiques à transporter dans une URL.
ASP.NET Core pouvait déjà router un verbe arbitraire avec MapMethods. L’évolution concerne notamment sa reconnaissance dans le document OpenAPI. La spécification OpenAPI 3.2 possède un champ query dédié. Avec les versions antérieures, ASP.NET Core utilise une extension du document.
Je détaille le fonctionnement de cette méthode, ses usages et les points de compatibilité dans mon article consacré à HTTP QUERY.
EF Core 11 et les accès aux données
EF Core 11 requiert .NET 11. Son adoption doit être coordonnée avec celle du runtime.
Les types complexes deviennent plus faciles à exploiter
Les types complexes représentent des groupes de valeurs sans identité propre, comme des coordonnées. Ils existaient déjà avant EF Core 11. Le mappage JSON avait également progressé dans EF Core 10.
EF Core 11 étend ces possibilités. Les propriétés scalaires contenues dans un type complexe peuvent notamment participer à des clés ou à des index. La configuration accepte aussi davantage de chemins d’accès directs.
Pour une entité Demandeur possédant une propriété complexe Coordonnees, la configuration peut prendre cette forme dans OnModelCreating :
1
2
3
4
5
6
7
8
9
constructeurModele.Entity<Demandeur>()
.ComplexProperty(demandeur => demandeur.Coordonnees);
constructeurModele.Entity<Demandeur>()
.Property(demandeur => demandeur.Coordonnees.Courriel)
.HasMaxLength(254);
constructeurModele.Entity<Demandeur>()
.HasIndex(demandeur => demandeur.Coordonnees.CodePostal);
L’exemple conserve le mappage des coordonnées en colonnes. Créer un index sur un chemin dans une colonne JSON demande, en plus, le support approprié du fournisseur et du moteur de base de données.
Les nouveautés d’EF Core 11 comprennent aussi le support des types complexes et des colonnes JSON dans les hiérarchies TPT et TPC, ainsi que leur prise en charge par le fournisseur Azure Cosmos DB.
Pour un modèle existant, je commencerais par un objet valeur représentatif. La migration devrait être examinée avec les requêtes, les mises à jour et le suivi des modifications qui le concernent. La correspondance entre une notion métier et un type complexe est plus importante que la réduction du nombre de lignes de configuration.
Du SQL plus sobre pour les chargements associés
EF Core 11 évite certaines jointures inutiles lors de requêtes fractionnées et retire des colonnes de tri redondantes liées aux navigations de référence.
Considérons une liste de dossiers avec leur demandeur et leurs documents :
1
2
3
4
5
var dossiers = await contexte.Dossiers
.Include(dossier => dossier.Demandeur)
.Include(dossier => dossier.Documents)
.AsSplitQuery()
.ToListAsync(cancellationToken);
Dans les scénarios concernés, la requête qui charge la collection n’a plus besoin de répéter toutes les jointures de référence. Le SQL contient moins de travail sans imposer une réécriture de l’expression LINQ.
Cela ne rend pas AsSplitQuery() préférable dans tous les cas. Les compromis des requêtes fractionnées demeurent, notamment les allers-retours supplémentaires et la cohérence entre plusieurs lectures en présence d’écritures concurrentes.
Je comparerais les plans d’exécution et le volume de données transférées sur les écrans réellement problématiques. Une collection volumineuse, plusieurs collections et une simple navigation de référence ne posent pas le même problème.
Des traductions LINQ supplémentaires
EF Core 11 sait traduire le nouvel opérateur FullJoin en jointure externe complète pour les bases relationnelles prises en charge. La version étend aussi la traduction de GroupBy, de MinBy et de MaxBy, et élimine certains CAST superflus.
Ces ajouts peuvent simplifier une requête existante ou éviter un contournement. Je vérifierais malgré tout le SQL généré avec le fournisseur et la version du moteur réellement utilisés. La présence d’une méthode dans LINQ ne garantit pas la même traduction partout.
Pour une migration, mes priorités resteraient les requêtes fréquentes, les traitements volumineux et les opérations déjà sensibles en production. Les améliorations du générateur SQL sont intéressantes, mais elles ne remplacent ni un index approprié ni une projection limitée aux données nécessaires.
Les correctifs d’EF Core dans la RC 2 concernent aussi SaveChanges, notamment l’ordre de certaines mises à jour portant sur des valeurs couvertes par un index unique et la résolution de cycles entre commandes. Un autre correctif permet de combiner une collection complexe stockée en JSON et une navigation de collection dans une projection. Ce sont de bons candidats pour les essais de non-régression d’une application qui avait déjà adopté une préversion.
Blazor : des améliorations concrètes
Des listes virtualisées plus faciles à maîtriser
Le composant Virtualize<TItem> reçoit des possibilités utiles pour les listes qui évoluent pendant leur consultation.
AnchorMode permet de contrôler l’ancrage au début ou à la fin de la liste lorsque des éléments sont ajoutés. Un journal d’activité peut ainsi suivre les nouvelles entrées tant que l’utilisateur reste près de la fin, puis le laisser consulter l’historique lorsqu’il remonte.
La méthode ScrollToItemAsync permet aussi de demander le déplacement vers un élément. C’est pertinent lorsqu’une action doit ramener l’utilisateur vers une ligne précise.
Je vois surtout un gain pour les écrans de suivi, les historiques et les files de travail. La virtualisation du rendu ne dispense toutefois pas de maîtriser la quantité de données demandée au serveur.
QuickGrid stabilise ses paramètres de virtualisation
Dans la RC 2, les paramètres AnchorMode et ItemComparer de QuickGrid ne portent plus l’attribut expérimental associé au diagnostic ASP0030. Leur utilisation n’exige donc plus de supprimer ce diagnostic.
Pour une grille déjà adaptée aux préversions, je retirerais les suppressions devenues inutiles et je vérifierais le comportement de défilement lors du rafraîchissement des données. La RC 2 corrige aussi l’ancrage en fin de liste de Virtualize après le premier chargement du fournisseur.
Des données temporaires en rendu serveur statique
Blazor ajoute TempData au rendu serveur statique pour transporter des informations temporaires entre des requêtes, par exemple après une soumission suivie d’une redirection.
Un message de confirmation peut être reçu dans un composant :
@if (!string.IsNullOrWhiteSpace(MessageConfirmation))
{
<p role="status">@MessageConfirmation</p>
}
@code {
[SupplyParameterFromTempData]
public string? MessageConfirmation { get; set; }
}
Cette possibilité évite de détourner l’URL ou de conserver longtemps un état qui n’a besoin de survivre qu’à une transition. Je la réserverais aux informations temporaires de présentation, comme une confirmation de sauvegarde.
Préparer les circuits aux déploiements
La gestion d’état côté serveur permet au serveur de demander la mise en pause des circuits avec Circuit.RequestCircuitPauseAsync.
Cela peut aider lors d’un arrêt planifié ou du retrait progressif d’une instance. L’application doit néanmoins organiser la persistance de l’état pertinent et laisser le temps au client de participer. Une demande acceptée ne signifie pas que la mise en pause est déjà terminée.
Pour un formulaire long, je conserverais une sauvegarde métier adaptée. Un mécanisme de reconnexion ne doit pas être la seule protection des données saisies.
Des formulaires plus réactifs
Le rendu serveur statique peut produire une validation côté navigateur à partir des règles prises en charge. L’utilisateur peut ainsi recevoir un retour immédiat pour certaines erreurs sans attendre un aller-retour.
Par ailleurs, les formulaires prennent en charge les règles asynchrones. La soumission attend leur résultat et les composants peuvent rendre visible une validation en cours.
Ces deux évolutions ont des responsabilités différentes. Une règle qui interroge un service reste dépendante de ce service. La validation dans le navigateur améliore l’expérience, tandis que les contrôles nécessaires à l’acceptation de la commande doivent toujours être exécutés côté serveur.
WebAssembly et politiques CSP
Les publications WebAssembly bénéficient d’un meilleur retrait du code de métriques et du code de Hot Reload lorsque ces fonctions ne sont pas utilisées.
Le modèle de navigation et la virtualisation évoluent également pour mieux fonctionner avec des politiques Content Security Policy restrictives. Le modèle de navigation évite notamment un gestionnaire JavaScript directement placé dans le HTML.
Cela réduit certains ajustements nécessaires lors du durcissement d’une application. Une application existante doit néanmoins adopter les changements de son modèle et vérifier ses propres scripts, styles et bibliothèques de composants. Une mise à niveau du runtime ne corrige pas automatiquement le contenu déjà présent dans le projet.
SDK et expérience développeur
Les essais automatisés et leur exécution
Le SDK .NET 11 poursuit l’intégration de Microsoft.Testing.Platform. Le modèle xUnit permet de choisir explicitement xUnit v3, qui utilise alors cette plateforme par défaut :
1
dotnet new xunit --xunit-version v3 -n GestionDossiers.Essais
dotnet test reçoit également de nouvelles possibilités pour piloter une exécution, gérer ses résultats et suivre les essais en cours. Cela peut simplifier le diagnostic d’un pipeline long ou d’une exécution répartie sur plusieurs modules.
Je profiterais d’une mise à niveau pour vérifier les commandes du pipeline avec le moteur réellement utilisé. Comme le rappelle la documentation de dotnet test, les options disponibles dépendent de VSTest ou de Microsoft.Testing.Platform. Changer de SDK, de moteur et de version majeure de xUnit en une seule étape rendrait les régressions plus difficiles à isoler.
Les applications basées sur des fichiers évoluent
Introduites dans .NET 10, les applications basées sur des fichiers gagnent notamment la directive #:include. Un petit outil peut répartir son code dans plusieurs fichiers tout en conservant ce mode de lancement.
Dans InspectionDossiers.cs :
1
2
3
4
#:include Modeles/DossierSimple.cs
var dossier = new DossierSimple("DOS-001", "Brouillon");
Console.WriteLine($"{dossier.Numero} : {dossier.Etat}");
Dans Modeles/DossierSimple.cs :
1
public sealed record DossierSimple(string Numero, string Etat);
Puis :
1
2
dotnet run --file InspectionDossiers.cs
dotnet format InspectionDossiers.cs --verify-no-changes
Les notes de la RC 1 pour le SDK mentionnent aussi la réutilisation de sorties Native AOT pour certains lancements et le support de ces fichiers par dotnet format.
La RC 2 permet en plus de placer entre guillemets les valeurs des directives #:property, notamment lorsqu’elles contiennent des espaces :
1
2
#:property Company="CodeWithFrenchy"
#:property AssemblyTitle="Inspection des dossiers"
Ces directives se placent au début du fichier. Les directives #:package, #:project et #:ref acceptent aussi des métadonnées MSBuild sous la forme Nom=Valeur, par exemple pour définir un alias de référence.
J’y vois un format utile pour un diagnostic reproductible, une transformation ponctuelle ou un outil interne simple. Lorsque l’outil acquiert plusieurs responsabilités, une configuration importante et une suite d’essais conséquente, un projet classique peut redevenir plus facile à maintenir.
Réutiliser les propriétés MSBuild dans les profils de lancement
La RC 2 permet à dotnet run de développer les propriétés MSBuild présentes dans commandLineArgs. Un profil de Properties/launchSettings.json peut donc utiliser une valeur déjà définie par le projet :
1
2
3
4
5
6
7
8
{
"profiles": {
"InspectionDossiers": {
"commandName": "Project",
"commandLineArgs": "--nom $(AssemblyName)"
}
}
}
Au lancement avec dotnet run --launch-profile InspectionDossiers, $(AssemblyName) est remplacé par la valeur évaluée du projet. L’application doit évidemment prendre en charge l’argument --nom.
MSBuild accélère l’évaluation de certains projets
La RC 2 améliore la recherche des fichiers correspondant aux motifs génériques, comme **/*.cs, pendant l’évaluation des projets. Dans les scénarios Windows mesurés par l’équipe, cette phase est de 10 à 13 % plus rapide et alloue de 16 à 34 % moins de mémoire.
Ces résultats concernent les scénarios d’évaluation publiés. Ils ne représentent pas une accélération équivalente de toute la compilation. Sur un dépôt volumineux, je comparerais le temps d’évaluation et le temps total du pipeline pour savoir où se situe le bénéfice.
Configurer un âge minimal pour les paquets NuGet
Les commandes NuGet de la RC 2 permettent de configurer un délai après la publication des paquets. Pour une source déjà nommée nuget.org dans le fichier nuget.config du dépôt :
1
dotnet nuget update source nuget.org --min-publish-age-hours 48 --configfile nuget.config
La valeur minPublishAgeHours guide la sélection des versions par dotnet package add et dotnet package update. Pour les projets utilisant des versions flottantes, une restauration échoue avec NU1020 si le délai ne laisse aucune version admissible.
J’y vois un contrôle utile pour éviter l’adoption immédiate de chaque nouvelle publication dans les mises à jour automatisées. Le délai de 48 heures est un exemple à adapter à la politique du dépôt. Il complète la revue des dépendances et l’analyse des vulnérabilités, sans garantir qu’un paquet plus ancien est sûr. Une équipe doit aussi prévoir comment traiter un correctif urgent.
CLI et publication de conteneurs
La commande dotnet run -e permet de passer des variables d’environnement au processus lancé :
1
dotnet run -e ASPNETCORE_ENVIRONMENT=Development
Le SDK ajoute aussi la gestion des filtres de solution .slnf par la CLI. Pour un grand dépôt, cela facilite le travail sur un sous-ensemble de projets.
La publication de conteneurs progresse sur la reproductibilité. SOURCE_DATE_EPOCH permet de stabiliser les horodatages utilisés, et la publication vers un registre peut éviter du travail lorsque le manifeste existe déjà. Les entrées de la construction, dont l’image de base et les dépendances, doivent également rester identiques pour obtenir un résultat reproductible.
Dans un pipeline d’entreprise, je fixerais explicitement les versions d’images et de SDK. Ces améliorations prennent leur sens lorsque deux constructions censées produire le même livrable peuvent effectivement être comparées.
Préparer l’adoption de .NET 11
STS ou LTS : regarder le calendrier réel
Selon la politique de support .NET, les versions paires sont LTS et les versions impaires STS. Les premières bénéficient de trois ans de support, les secondes de deux ans. Microsoft précise que la distinction porte sur la durée de support, pas sur un niveau de qualité différent.
Voici la situation au moment de la rédaction :
| Version | Type | Horizon de support |
|---|---|---|
| .NET 8 | LTS | 10 novembre 2026 |
| .NET 9 | STS | 10 novembre 2026 |
| .NET 10 | LTS | 14 novembre 2028 |
| .NET 11 | STS | 9 novembre 2028, selon le calendrier annoncé |
Le calendrier annoncé pour .NET 11 prévoit deux ans de support, du 10 novembre 2026 au 9 novembre 2028. .NET 10 et .NET 11 arrivent donc toutes deux en fin de support en novembre 2028. Le choix se fera surtout sur la valeur des nouveautés et la capacité de l’équipe à maintenir sa cadence de mises à niveau.
Pour une application encore sur .NET 8 ou 9, je sécuriserais d’abord une cible prise en charge avant l’échéance de novembre. Une migration déjà validée vers .NET 10 conserve sa valeur. L’arrivée imminente de .NET 11 ne constitue pas, à elle seule, une raison de la repousser.
Partir de composants Microsoft à jour
Avant de changer de version majeure, je commencerais par installer les derniers correctifs disponibles pour les composants Microsoft utilisés, dans leurs branches de version actuelles. Cela concerne notamment le SDK, le runtime .NET, ASP.NET Core et les paquets NuGet comme EF Core ou Microsoft.Extensions.*, en conservant des versions compatibles entre elles.
Je validerais cette mise à jour avec les essais existants avant de modifier la cible .NET. On part ainsi d’une application qui bénéficie déjà des corrections de bogues disponibles, avec une référence plus fiable pour évaluer la migration. Cela réduit aussi le risque d’attribuer à .NET 11 un problème déjà corrigé dans la version précédente.
Vérifier toute la chaîne de livraison
Je préparerais la migration autour des vérifications suivantes :
| Élément | Vérification concrète |
|---|---|
| SDK et compilation | Fixer la version dans global.json et l’installer dans les agents de construction |
| Dépendances | Vérifier les paquets NuGet, fournisseurs EF Core, générateurs de code et analyseurs |
| Hébergement | Confirmer la prise en charge du mode de déploiement et du runtime par le service cible |
| Contrats | Comparer les documents OpenAPI, les représentations JSON et les réponses d’erreur |
| Données | Examiner les migrations, les requêtes critiques et leurs plans d’exécution |
| Exploitation | Comparer les mesures de référence, les journaux et les traces avant le déploiement général |
Pour Azure App Service ou Azure Functions, je vérifierais les matrices de support au moment de la migration. La sortie générale de .NET ne signifie pas que chaque mode d’hébergement ou chaque composant de l’écosystème le propose immédiatement.
Un global.json peut fixer le SDK .NET 11 RC 2 pour une branche d’évaluation :
1
2
3
4
5
6
7
{
"sdk": {
"version": "11.0.100-rc.2.26504.105",
"rollForward": "disable",
"allowPrerelease": true
}
}
Cette configuration exige exactement le SDK indiqué. Elle rend l’expérience reproductible, mais impose de mettre à jour le fichier et les agents lors du passage à la version finale, puis selon la politique de maintenance retenue. Elle ne constitue donc pas une politique à figer indéfiniment.
Pour l’évaluation de la RC 2 sous Windows, Microsoft recommande Visual Studio 2026 Insiders. Visual Studio Code avec C# Dev Kit constitue aussi une option prise en charge.
Examiner les changements incompatibles et les prérequis matériels
La liste des changements incompatibles de .NET 11 doit faire partie de la préparation. J’y ajouterais les notes propres à ASP.NET Core et EF Core, ainsi que celles des principales dépendances du projet.
Un changement mérite une attention particulière pour les infrastructures anciennes : les prérequis matériels évoluent. Pour x86/x64, le niveau minimal inclut désormais les instructions du niveau x86-64-v2.
Il faut distinguer ce minimum des cibles ReadyToRun utilisées pour optimiser des binaires précompilés. Sous Windows et Linux, ces cibles montent à x86-64-v3, sans faire d’AVX2 une exigence minimale générale pour toute application .NET 11. Du matériel pris en charge peut nécessiter davantage de compilation JIT au démarrage.
Dans un parc virtualisé, je vérifierais les fonctionnalités CPU réellement exposées aux machines virtuelles. Un hôte récent peut présenter un profil plus ancien à ses invités.
Adopter les nouveautés progressivement
Avec les versions récentes de .NET, les migrations sont généralement relativement transparentes pour les applications maintenues à jour. Dans bien des projets, l’essentiel consiste à mettre à jour le SDK, le framework ciblé et les paquets concernés, puis à exécuter les essais. Les changements incompatibles et les particularités de l’hébergement restent à vérifier, mais une migration bien préparée peut rester une opération de maintenance courante.
Je ferais donc d’abord la mise à niveau technique, puis j’introduirais les nouvelles fonctionnalités selon les besoins du projet. Le passage à .NET 11 permet déjà de bénéficier de nombreuses améliorations du runtime et des bibliothèques avec le code existant.
Les unions peuvent être introduites sur une commande bien délimitée. Les hiérarchies fermées peuvent améliorer un modèle d’états. Runtime Async peut être évalué sur un service représentatif, avec sa propre comparaison de performances.
Cette progression permet de relier chaque décision à un résultat observable. Elle évite aussi qu’un changement de syntaxe, une nouvelle représentation JSON et une évolution du moteur d’exécution arrivent simultanément sur le même parcours critique.
Conclusion
Ce qui m’intéresse le plus dans .NET 11, c’est le rapprochement entre l’intention du code et ce que les outils peuvent en comprendre.
Les types union rendent les résultats attendus plus visibles. Les hiérarchies fermées donnent au compilateur une meilleure connaissance des alternatives. Runtime Async permet au moteur d’exécution de travailler sur une représentation plus riche du code asynchrone. EF Core, la validation et Blazor prolongent cette évolution dans des scénarios applicatifs très concrets.
Mes premières évaluations porteraient sur les unions pour les résultats métier, les améliorations SQL d’EF Core et Runtime Async sur un service mesurable. Pour les équipes Blazor, la validation et la gestion des circuits peuvent aussi répondre à des irritants quotidiens.
Je garderais toutefois la même grille de décision que pour toute modernisation : un bénéfice identifiable, une compatibilité vérifiée et une capacité à maintenir la solution après la migration. Une nouveauté a surtout de la valeur lorsqu’elle rend notre application plus claire, plus efficace ou plus simple à exploiter.
.NET 11 offre plusieurs occasions de progresser sur ces trois axes. Avec la RC 2 disponible et une sortie générale prévue le 10 novembre, c’est un bon moment pour valider les parcours critiques sur nos propres applications et préparer une adoption adaptée à notre contexte.