Public cible
Débutants, étudiants et professionnels qui souhaitent acquérir une pratique structurée dans ce domaine.
Maîtriser C# moderne et ASP.NET Core : nullabilité vérifiée par le compilateur, asynchrone avec annulation, architecture en couches, injection de dépendances, Entity Framework Core, tests et mise en production. Le cours s'ancre sur la version LTS en vigueur, dont la date de fin de support est un critère d'architecture.
Débutants, étudiants et professionnels qui souhaitent acquérir une pratique structurée dans ce domaine.
Comprendre les notions essentielles et les appliquer dans un projet vérifiable.
Aucune connaissance préalable n’est strictement obligatoire ; les prérequis détaillés sont précisés dans les ateliers.
Tous niveaux
La validation repose sur les évaluations actives et les livrables pratiques du cours.
Un ordinateur, un navigateur et les outils indiqués dans le premier atelier sont suffisants pour commencer.
Écrire une API avec transaction, annulation et tests de cas limites.
Obligatoire ConsulterÉvaluer conception, concurrence, requêtes et déploiement.
Obligatoire ConsulterÉcrire une API avec transaction, annulation et tests de cas limites.
Obligatoire ConsulterÉvaluer conception, concurrence, requêtes et déploiement.
Obligatoire ConsulterÉcrire une API avec transaction, annulation et tests de cas limites.
Obligatoire ConsulterÉvaluer conception, concurrence, requêtes et déploiement.
Obligatoire ConsulterÉcrire une API avec transaction, annulation et tests de cas limites.
Obligatoire ConsulterÉvaluer conception, concurrence, requêtes et déploiement.
Obligatoire ConsulterÉcrire une API avec transaction, annulation et tests de cas limites.
Obligatoire ConsulterÉvaluer conception, concurrence, requêtes et déploiement.
Obligatoire ConsulterUne version sortie de support ne reçoit plus de correctifs de sécurité. C'est le seul critère qui doit décider, parce qu'il conditionne l'extérieur du projet : la conformité, la sécurité, la responsabilité contractuelle.
Une version majeure par an, en novembre. L'alternance est fixe : les années impaires donnent une version LTS (support de trois ans), les années paires une version STS (deux ans). La qualité est identique dans les deux cas — seule la durée de support diffère. Un cours qui laisse croire qu'une version STS est moins bonne se trompe.
| Version | Type | Phase | Fin de support |
|---|---|---|---|
| .NET 10 | LTS | active | 14 novembre 2028 |
| .NET 9 | STS | maintenance | 10 novembre 2026 |
| .NET 8 | LTS | maintenance | 10 novembre 2026 |
La ligne à retenir pour un projet qui démarre : viser la LTS en vigueur, ici .NET 10. Deux conséquences pratiques :
Le tableau se vérifie en deux clics sur la page de support officielle. Le recopier ici ne le périme pas : c'est la page qui fait foi, et c'est elle qu'un projet doit consulter au moment de son arbitrage.
C# est à typage statique : le type est connu à la compilation, et le vérificateur de nullité est activé par défaut. Ce second point est un changement majeur par rapport à l'usage courant du langage.
Un type de valeur contient sa donnée ; un type de référence contient une adresse. La conséquence : copier un type de valeur duplique la donnée, copier un type de référence copie l'adresse — les deux pointent alors vers le même objet. Comprendre cette différence évite une famille entière de bugs où l'on croit modifier une copie.
Une référence peut ne pointer vers rien. Le langage
le signale dans le type : string garantit
non nul, string? indique qu'il peut
l'être. L'avertissement n'est pas un avertissement,
c'est une erreur de compilation.
C'est le meilleur rapport qualité/prix de l'écosystème C# moderne : impossible de déréférencer une référence nulle sans que le compilateur l'ait signalé. Ce qui n'est pas traité reste à traiter — avec un motif explicite, jamais un simple null-forgiving operator posé pour faire disparaître l'erreur.
Un record définit une valeur dont l'identité
est son contenu, pas sa référence. Il est par défaut
immuable et fournit nativement l'égalité par valeur,
que les classes ne donnent pas. Pour un objet de
transfert — un résultat de calcul, une ligne de
commande, une réponse d'API, c'est le bon choix par
défaut, et l'expression with permet
de dériver une version modifiée sans muter
l'original.
Attention à ne pas confondre : record class
reste une référence, record struct est un
type de valeur. Le choix change la sémantique de
passage.
Toute opération réseau ou d'entrée-sortie est
asynchrone. Le mot-clé async/await
libère le thread pendant l'attente, ce qui permet à un
serveur de servir des milliers de requêtes avec peu de
threads.
Bloquer sur une tâche asynchrone avec
.Result ou .Wait() provoque
un interblocage dans un contexte de
synchronisation : le thread qui doit exécuter la
suite attend un résultat qui ne peut pas être produit,
parce que le thread nécessaire est bloqué. Le programme
se fige, sans message d'erreur. C'est le piège le plus
coûteux du langage, parce qu'il ne se manifeste qu'en
application web, jamais dans un exécutable
console.
Une opération asynchrone doit accepter un jeton d'annulation et le transmettre à toutes les opérations en aval. C'est ce qui permet, lorsqu'un utilisateur ferme un onglet ou change de page, d'interrompre le travail au lieu de le laisser s'exécuter jusqu'au bout pour un résultat que plus personne ne lira. Le serveur ne s'en rend pas compte tout seul.
La règle : une signature asynchrone qui ne prend pas de jeton d'annulation est incomplète.
async sans await génère un
avertissement et s'exécute de façon synchrone,
ce qui crée une attente bloquante déguisée.La règle de conception qui évite les deux : propager le jeton d'annulation jusqu'aux feuilles de l'arbre d'appels, plutôt que de l'absorber au niveau du service. Une exception capturée et ignorée en cours de route est le moyen le plus sûr de perdre une annulation sans le voir.
Les principes SOLID sont des critères de conception, pas des recettes à cocher. Savoir les nommer ne sert à rien ; savoir à quel défaut d'architecture chacun répond, oui.
| Principe | Le symptôme qu'il traite |
|---|---|
| Responsabilité unique | une classe qui change pour trois motifs non liés ; c'est elle qu'on ouvre a chaque nouvelle demande |
| Ouvert/fermé | chaque nouveau cas oblige à modifier un code existant qui fonctionnait |
| Substitution | une hiérarchie où la sous-classe casse l'attente de la classe mère |
| Interface segregation | un client forcé d'implémenter des méthodes qu'il n'utilise pas |
| Inversion des dépendances | un composant de haut niveau qui dépend d'un composant de détail |
Le symptôme le plus cité est « la classe trop grosse ». Le vrai problème n'est pas sa taille : c'est qu'elle change pour des raisons sans rapport. Une classe de six cents lignes qui gère une seule chose est un bon candidat ; une classe de quatre-vingts lignes dont la moitié des méthodes ne sert à personne est un mauvais.
Une formulation courte : « Cette classe changerait-elle si l'exigence changeait ? » Si oui, la raison du changement est une responsabilité distincte. Appliquée à un service qui valide, calcule, enregistre et envoie un e-mail, elle isole les quatre en une réponse, sans diagramme.
Découper systématiquement produit ce que la littérature appelle la dispersion conceptuelle : vingt fichiers pour une fonctionnalité, et plus personne ne sait où chercher. Le découpage se justifie quand la séparation correspond à deux notions qui changent pour deux raisons différentes. Sinon, on a ajouté de la complexité d'injection sans aucun bénéfice.
L'injection de dépendances est le mécanisme technique qui rend possible l'inversion des dépendances. Le principe dit qu'un composant de haut niveau ne doit pas créer lui-même ce dont il dépend ; le mécanisme dit comment on lui fournit.
Écrire new HttpClient() dans une
méthode n'est pas « utiliser une dépendance », c'est
créer la dépendance sur place. Le composant est
alors lié à un type concret et à son cycle de vie, et il
devient impossible à tester sans déclencher un accès
réseau réel.
Le conteneur d'inversion de contrôle fourni avec l'écosystème résout ce cas : la dépendance est déclarée dans le constructeur, et le conteneur fournit l'instance. Le service ignore tout du cycle de vie, et un test lui passe une doublure.
La fuite d'état entre requêtes est le bug le plus coûteux de ce mécanisme, et il est invisible en développement mono-utilisateur. Il apparaît en production avec un second utilisateur, puis devient impossible à reproduire en recette.
Une application se lit en couches. L'erreur classique est de les confondre : le modèle métier qui connaît le format JSON, ou le contrôleur qui contient une règle de gestion.
Les dépendances pointent toujours vers l'intérieur. La couche API connaît le service, le service connaît le domaine, le domaine ne connaît personne. Cette règle Unique et simple rend le cœur métier testable sans base de données, sans réseau et sans framework — ce qui est la définition même d'un code testable rapidement.
Le domaine a besoin d'un concept externe : un identifiant unique, un horodatage, une unité monétaire. Il ne doit pas pour autant importer la bibliothèque qui le fournit. On déclare une abstraction dans le domaine, on fournit l'implémentation par injection. La règle reste respectée, et le domaine compilera seul.
Un objet entité repris tel quel dans une réponse JSON va exposer ce qu'il ne devrait pas : un identifiant technique interne, une date de mise à jour, parfois un hash de mot de passe ou un numéro de sécurité sociale si le mapping est baclé. La séparation entre modèle de persistance et modèle d'échange n'est pas de la sur-ingénierie : c'est ce qui évite qu'une migration de base de données casse une API publique.
Une couche de plus n'est pas automatiquement mieux. Trois couches — API, application, domaine — suffisent à la quasi-totalité des cas. Le critère de découpage n'est jamais le nombre de couches, mais le fait qu'une couche puisse être remplacée indépendamment : un jour où la base change, seul le dépôt doit bouger.
Une requête ASP.NET Core traverse un pipeline ordonné. Chaque élément peut agir sur la requête avant qu'elle n'atteigne le contrôleur, ou sur la réponse au retour.
Un intercepteur d'authentification placé après l'autorisation ne protège plus de rien ; une journalisation placée après le gestionnaire d'exceptions n'enregistrera pas les erreurs non gérées. Un composant mal placé est un composant inopérant, sans le moindre avertissement. C'est la première cause d'incident de production sur un pipeline d'authentification.
Le piège le plus coûteux du modèle est l'exception filters : par défaut, une exception non gérée dans un contrôleur court-circuite la chaîne et part directement vers le middleware global. Le filtre d'exception placé sur le contrôleur n'est jamais appelé — la documentation le signale, et c'est la première cause d'erreurs rendues en 500 sans corps utile, alors que le gestionnaire prévu existe.
Deux remèdes : rechercher explicitement l'exception dans le gestionnaire global, ou activer le court-circuit explicite. Le choix se documente, parce qu'il change le comportement du pipeline entier.
Réflexe de vérification : la même exécution doit fonctionner avec deux fichiers de configuration différents, sans recompilation. Si le build produit exactement le même artefact partout, la configuration n'est pas injectée correctement.
Une API doit renvoyer un code de statut cohérent avec la situation : 400 pour une demande invalide, 401 pour une absence d'authentification, 403 pour un droit insuffisant, 404 pour une ressource absente, 409 pour un conflit d'état, 500 pour une erreur serveur. Le distinguer n'est pas cosmétique : 401 et 403 ne se traitent pas de la même façon côté client, et les confondre oblige le consommateur à deviner s'il doit rediriger vers une page de connexion ou afficher un message d'interdiction.
Entity Framework Core est un mappeur relationnel : des objets en mémoire, des tables en base. Toute la difficulté tient à la frontière entre les deux mondes.
Une requête LINQ est évaluée au dernier moment quand le résultat est parcouru, pas à l'écriture. Deux conséquences qu'il faut connaître par cœur :
Le remède est l'inclusion explicite des relations nécessaires, ou le chargement paresseux activé par défaut avec l'inclusion de toutes les relations. Choisir l'un plutôt que l'autre est un arbitrage : le second est simple, le premier est prévisible.
Par défaut, Entity Framework suit les entités lues : il conserve leur état initial et applique automatiquement les modifications détectées au moment de l'enregistrement. Conséquence directe et peu évidente : modifier une propriété sur un objet lu suffit à déclencher une mise à jour, sans appel explicite. C'est confortable, et c'est dangereux : une propriété de navigation qu'on touche par erreur produit une écriture inattendue en base.
La parade : utiliser des requêtes sans suivi pour la lecture seule, et traiter l'écriture dans un composant dédié et explicite.
Par défaut, aucune transaction n'est ouverte
autour d'un SaveChanges pris isolément.
Dans une opération qui modifie plusieurs tables, un
échec au milieu laisse un état intermédiaire
incohérent : la commande est enregistrée mais pas son
ligne de détail. La transaction explicite autour de
l'ensemble est l'une des trois seules garanties
d'intégrité, avec les clés étrangères et les
contraintes d'unicité.
Le générateur de SQL produit toujours des requêtes paramétrées : les valeurs ne sont jamais concaténées dans le texte SQL. C'est une garantie de l'ORM, et elle protège de l'injection à condition de ne pas contourner le générateur. Dès qu'on écrit du SQL à la main, la securite redevient une responsabilité du développeur, avec le paramétrage explicite comme seule parade.
Le langage rend le test unitaire simple, parce que le code se décompose en méthodes pures sans état caché. Cette simplicité a un revers : il est tentant de tester des détails d'implementation, ce qui produit une suite lente et fragile sans protéger quoi que ce soit.
La question n'est jamais « faut-il tester ça ? » mais « quel comportement doit rester vrai, et comment saurai-je qu'il est rompu ? »
Test vérifié : si le comportement change et que le test échoue, il fait son travail ; s'il échoue pour une raison sans rapport avec le comportement, il coûte.
Le runtime asynchrone rend l'accès concurrent naturel : deux requêtes peuvent s'exécuter entrelacées dans le même processus. Un code écrit pour un seul utilisateur à la fois fonctionne en développement et échoue en production.
Par défaut, un service doit être avec portée — une instance par requête. C'est la seule portée qui garantit l'isolation entre utilisateurs. La portée d'application ne se justifie que pour un service explicitement sans état, ce qui se vérifie en relisant ses champs.
Un test de concurrence n'est pas déterministe, et c'est normal : il ne prouve pas l'absence de bug, il prouve qu'un scénario précis échoue. Un test qui passe une fois sur cent n'est pas rassurant, il est informatif.
La publication est l'étape où les hypothèses de développement se vérifient. Ce qui a été supposé disponible localement ne l'est pas forcément.
Le point qui manque le plus souvent : une réponse 200 ne prouve pas que l'application fonctionne. Un 200 avec un corps d'erreur JSON, ou une page vide parce que la requête de données a échoué silencieusement, passe tous les tests de disponibilité. La vérification utile lit la réponse.
Le taux d'erreur par route et par code de statut, la latence au 95ᵉ centile — la moyenne masque les extrêmes —, la consommation mémoire, et le nombre de requêtes base de données par requête applicative. Ce dernier ne se voit pas dans les journaux applicatifs : il faut l'outillage de la base, et c'est là que traîne l'essentiel des problèmes de performance.
Une API web en .NET sur la LTS en vigueur, avec authentification, gestion des rôles, opérations CRUD, pagination, filtres, et une base de données réelle. Le livrable comprend les migrations, les tests et la procédure de déploiement.
Le jury retient une chose : l'erreur est-elle détectable et compris ? Une application qui plante proprement avec un message exploitable vaut mieux qu'une application qui semble fonctionner et livre des incohérences. Les deux premiers critères techniques après cela sont l'annulation asynchrone et la transaction — les deux défauts que les clients signalent le plus souvent après livraison.
Ecrire une API sur la LTS en vigueur, en treatments qu’un client reel ne casse pas.
Publié le 29/09/2026