Sommaire
Introduction
L’objectif principal est de vous familiariser avec les concepts fondamentaux des services web (webservice), en mettant particulièrement l’accent sur la mise en pratique de ces connaissances à travers le langage de programmation C#.
Définition et Concepts de Base d’un Service Web
Qu’est-ce qu’un Service Web ?
Un service web est une application ou un composant logiciel accessible sur le réseau, souvent via Internet. Ces services sont conçus pour être interopérables, c’est-à-dire qu’ils peuvent être utilisés par des applications écrites dans des langages différents et déployées sur des environements divers. Ils permettent d’exposer des fonctionnalités ou des données à d’autres applications via des protocoles standardisés.
Principaux Concepts des Services Web
-
Interopérabilité :
- Les services web sont conçus pour être indépendants de la plateforme ou du langage de programmation, ce qui signifie qu’un service web écrit en C# peut être consommé par une application écrite en Java, Python, ou tout autre langage.
-
Communication Basée sur des standards et protocoles ouverts :
- Les services web utilisent les protocoles réseau standard pour la communication, comme HTTP/HTTPS via TCP pour la transmission des messages. Les protocoles et les formats de données sont au format texte dans la mesure du possible, facilitant ainsi la compréhension du fonctionnement global des échanges.
-
Architecture Orientée Services (SOA) :
- Les services web s’intègrent souvent dans une architecture orientée services (SOA), où des composants logiciels distincts communiquent via des services web pour accomplir des tâches spécifiques. Dans une SOA, chaque service est un module autonome qui remplit une fonction spécifique et expose une interface pour interagir avec d’autres services ou applications.
SOAP vs RESTful
Il existe deux principaux styles de services web :
SOAP (Simple Object Access Protocol) est un protocole standardisé qui utilise XML pour le format des messages et inclut des spécifications rigides pour l’échange d’informations structurées. Il est souvent utilisé dans des environnements où la sécurité et la complexité sont critiques.
RESTful (Representational State Transfer) est une architecture plus légère qui utilise les méthodes standard du protocole HTTP (GET, POST, PUT, DELETE) pour effectuer des opérations CRUD (Create, Read, Update, Delete). REST est apprécié pour sa simplicité et son efficacité, particulièrement dans le contexte des API web modernes.
Importance des Services Web
Les services web jouent un rôle essentiel dans le développement logiciel moderne, car ils permettent aux applications de s’intégrer et de collaborer de manière flexible et évolutive. Ils facilitent le développement d’architectures distribuées, la réutilisation des composants logiciels, et la communication entre systèmes disparates, ce qui est crucial dans les environnements de développement complexes d’aujourd’hui.
Dans les grandes structures, chaque service est géré de manière indépendante par une équipe dédiée. Cette approche permet de paralléliser le développement, rendant le processus plus efficace. En assignant à chaque service une responsabilité spécifique, on peut diviser les tâches en plusieurs petits projets autonomes, plutôt que de gérer un seul projet monolithique et complexe.
2 .NET et C#
Présentation de .NET
.NET est une plateforme de développement open-source, conçu et maintenue par Microsoft.
Elle permet de créer une large variété d’applications, notamment des applications web, mobiles, de bureau.
Elle supporte plusieurs langages de programmation, dont C#, F#, et Visual Basic.
.NET offre un environnement de développement unifié multi-plateformes (Windows, Linux, macOS). Les composants principaux de .NET incluent le .NET Runtime pour l’exécution des applications, ASP.NET Core pour le développement d’applications web, et Entity Framework Core pour l’accès aux données. Son architecture modulaire et sa compatibilité avec les services cloud en font un choix populaire pour le développement d’applications modernes et performantes.
Le .NET Runtime
Le .NET Runtime est le moteur d’exécution des applications .NET. Il s’agit d’un environnement qui gère l’exécution du code .NET, assure la gestion de la mémoire et gère les exceptions. Le runtime compile le code intermédiaire (Intermediate Language, ou IL) en code machine natif.
Entity Framework Core
Entity Framework Core est un ORM (Object-Relational Mapper) pour .NET, qui simplifie l’interaction avec les bases de données relationnelles. Il permet aux développeurs de manipuler les données sous forme d’objets C# sans avoir à écrire du SQL. Il supporte une variété de bases de données, telles que SQL Server, SQLite, PostgreSQL, et MySQL.
ASP.NET
ASP.NET est un framework de développement web, conçu par Microsoft, qui permet de créer des applications web modernes, dynamiques et évolutives. Il offre un ensemble complet d’outils et de bibliothèques pour le développement de sites web, d’API RESTful, et d’applications en temps réel. En intégrant des fonctionnalités comme la sécurité, l’authentification, et la gestion des sessions, ASP.NET simplifie le développement de solutions web robustes et performantes, adaptées aux besoins des entreprises modernes.
C#
Le langage en C# constitue le langage le plus connu pour la plateforme .NET. Il est très populaire et se pose en alternative a Java.
C# est un langage à usage général multiplateforme produisant du code hautement performant. C# est un langage orientés objet, il intègre de nombreuses fonctionnalités d’autres paradigmes, notamment la programmation fonctionnelle.
Setup
Installation de .NET
Pour ce projet, nous allons utiliser .NET 9.0. Pour l’installer, il suffit de suivre les instructions sur le site officiel de .NET.
Veuillez prendre la version SDK 9.0.306.
IDE
Vous pouvez utiliser l’IDE de votre choix, ou même un éditeur de texte.
Cependant, je vous recommande d’utiliser un des IDE suivant:
Visual Studio (Community) 2022
Visual Studio est un IDE complet qui permet de développer des applications en C# mais aussi en C++, F#, Python, etc. Développé par Microsoft, il est très complet. La version Community est gratuite et suffisante pour ce projet.
Rider
Rider est un IDE développé par JetBrains. Vous pouvez l’obtenir gratuitement si vous êtes étudiant.
VS Code
Si vous préférez utiliser un éditeur de texte, je vous recommande d’utiliser VS Code.
Il vous faudra installer les extensions suivantes:
https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscode-dotnet-pack https://marketplace.visualstudio.com/items?itemName=qwtel.sqlite-viewer
Installation de .NET SDK
Vous pouvez installer le SDK de .NET en utilisant la commande suivante: Ctrl+Shift+P -> .NET: Install net SDK

HTTP
HTTP (HyperText Transfer Protocol) est un protocole de communication client-serveur. Il est utilisé pour transférer des données sur le web. Il est basé sur le modèle requête-réponse. Il utilise le protocole TCP (Transmission Control Protocol) pour établir une connexion entre le client et le serveur.
Protocole
HTTP est un protocole sans état, ce qui signifie que chaque requête est traitée indépendamment des autres. Cela signifie que le serveur ne conserve pas d’informations sur les requêtes précédentes.
Une requête HTTP est composée de plusieurs parties :
- Ligne de requête : contient la méthode, l’URI et la version du protocole.
- En-têtes : contiennent des informations supplémentaires sur la requête.
- Corps : contient les données de la requête.
Une réponse HTTP est également composée de plusieurs parties :
- Ligne de statut : contient la version du protocole, le code de statut et le message de statut.
- En-têtes : contiennent des informations supplémentaires sur la réponse.
- Corps : contient les données de la réponse.
Méthodes HTTP
Il existe plusieurs méthodes HTTP, les plus courantes sont :
- GET : récupère des données à partir du serveur.
- POST : envoie des données au serveur pour traitement.
- PUT : met à jour des données sur le serveur.
- DELETE : supprime des données sur le serveur.
Il existe d’autres méthodes comme HEAD, OPTIONS, PATCH, etc.
Ces méthodes sont des conventions pour indiquer au serveur ce qu’il doit faire avec la requête, mais elles ne sont pas strictement suivies par tous les serveurs.
Codes de statut HTTP
Les codes de statut HTTP sont des codes numériques qui indiquent le résultat de la requête. Les codes de statut sont divisés en cinq catégories :
- 1xx : Informationnel
- 2xx : Succès
- 3xx : Redirection
- 4xx : Erreur du client
- 5xx : Erreur du serveur
Les codes de statut les plus courants sont :
- 200 OK : la requête a réussi.
- 201 Created : la ressource a été créée avec succès.
- 400 Bad Request : la requête est incorrecte.
- 401 Unauthorized : l’accès à la ressource est refusé.
- 404 Not Found : la ressource demandée n’a pas été trouvée.
- 500 Internal Server Error : une erreur interne du serveur s’est produite.
Exemple de requête HTTP GET
Voici un exemple de requête HTTP GET :
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: text/html
Dans cet exemple, le client envoie une requête GET pour récupérer le fichier index.html sur le serveur www.example.com.
User-Agent indique le navigateur utilisé par le client, et Accept indique le type de contenu accepté par le client.
Voici a quoi pourrait ressembler une réponse :
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 84
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
Dans cet exemple, le serveur envoie une réponse avec le code de statut 200 OK et le contenu HTML de la page.
Content-Type indique le type de contenu de la réponse, et Content-Length indique la longueur du contenu en octets.
Exemple de requête HTTP POST
Voici un exemple de requête HTTP POST :
POST /login HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Content-Type: application/json
Content-Length: 45
{ "username": "johndoe", "password": "1234" }
Dans cet exemple, le client envoie une requête POST pour se connecter au serveur www.example.com sur la route /login.
Le corps de la requête contient les informations de connexion de l’utilisateur au format JSON.
On précise le type de contenu avec Content-Type et la longueur du contenu avec Content-Length.
Voici a quoi pourrait ressembler une réponse :
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 52
{ "message": "Login successful", "token": "abc123" }
Exemple de réponse HTTP avec erreur
Voici un exemple de réponse HTTP avec une erreur :
HTTP/1.1 404 Not Found
Content-Type: text/plain
Content-Length: 13
Page not found
Dans cet exemple, le serveur envoie une réponse avec le code de statut 404 Not Found et le message Page not found.
Conclusion
HTTP est un protocole de communication client-serveur largement utilisé sur le web. Il permet de transférer des données de manière fiable et sécurisée entre les clients et les serveurs. Il est important de comprendre les méthodes HTTP, les codes de statut et les en-têtes pour développer des applications web efficaces et robustes.
Le language C#
Dans cette section, nous allons aborder les concepts de base du langage C#.
Vous y trouverez des exmples de code pour vous aider à comprendre les concepts.
Cette partie n’est pas exhaustive, elle a pour but de vous donner une idée générale du langage C#.
Je vous invite à consulter la documentation officielle pour plus d’informations.
C# Variables
Types
En C# les variables sont typés, voici une liste des types les plus commun
byte b = 255;
short s = -42;
ushort us = 65535;
int i = .1337;
uint ui = 1337;
long l = -13374269;
ulong ul = 13374269;
char c = 'a';
float f = 0.0f;
double d = 0.0d;
bool bo = false;
Tableau
Il est possible de créer des tableau via la syntaxe suivante :
// Crée un tableau de 10 int
int[] tableau = new int[10];
// Assigne 1 à l'index 0
tableau[0] = 1;
// Crée un tableau et l'initialise avec les éléments 0, 2, 4, 6, 8
int[] a = {0, 2, 4, 6, 8};
// Cette ligne équivaut à celle du dessus
int[] a = new int[] {0, 2, 4, 6, 8};
Console.WriteLine(a[1]); // 2
Multi dimensions
On peux également créer des tableaux à plusieurs dimensions.
// Crée un tableau a 2 dimension de 10 * 10
int[,] deuxDimensions = new int[10, 10];
Console.WriteLine(a[0, 0]); // 0
a[0, 0] = 1;
Console.WriteLine(a[0, 0]); // 1
Tableau de tableau
Un tableau multi dimension est différent d’un tableau de tableau. Voici comment créer un tableau de tableau si besoin
int[][] tableauCeption =
{
new int[] {1},
new int[] {1, 1},
new int[] {1, 2, 1},
new int[] {1, 3, 3, 1}
};
Console.WriteLine(tableauCeption[2][1]); // 2
tableauCeption[0][0] = 10;
Console.WriteLine(tableauCeption[0][0]); // 10
Nullables
Il est possible de déclarer une variable nullable en ajoutant ? après le type.
int i = 42;
i = null; // KO
int? nullable = 0;
nullable = null; // OK
Les conditions
Instruction if
int a = 8;
if (a % 2 == 0)
{
Console.WriteLine("Pair");
}
else
{
Console.WriteLine("Impair");
}
Instruction switch-case
int a = 42;
switch (a)
{
case 42:
Console.WriteLine($"21*2");
break;
case 1337:
Console.WriteLine("L33t");
break;
case > 69:
Console.WriteLine("Nice!");
break;
default:
Console.WriteLine($"{a} ne correspond a rien ici");
break;
}
Les boucles
break comme dans les conditions permet d’interrompre une boucle et d’en sortir
Instruction for
for (int i = 0; i < 3; i++)
{
Console.Write(i);
}
Instruction foreach
List<int> listeDeNombre = new() { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int nombre in listeDeNombre)
{
Console.Write(nombre);
}
// Typage automatique avec var
foreach (var nombre in listeDeNombre)
{
Console.Write(nombre);
}
Instruction while
int n = 0;
while (n < 5)
{
Console.Write(n);
n++;
}
Instruction do-while
int n = 0;
do
{
Console.Write(n);
n++;
} while (n < 5);
Instruction break
L’instruction break permet de sortir d’une boucle
while (true)
{
Console.WriteLine("Hello");
break; // on sort du while ici
Console.WriteLine("World"); // N'est jamais executé
}
Instruction continue
continue permet de sauter l’itération en cours et de passer a la suivante
for (int i = 0; i < 42; i++)
{
if (i == 2) continue; // si i vaut 2 on s'arrete la et on reprend avec i == 3
Console.Write(i);
}
Structure
Voici un exemple basique d’enumération
enum Saison
{
Printemps,
Ete,
Automne,
Hiver
}
Saison a = Season.Automne;
Console.WriteLine($"La valeur entière de {a} est {(int)a}"); // output: La valeur entière de Autumn est 2
On peut définir manuellement la valeur des membres de l’enum.
enum Saison : uint
{
Printemps = 0,
Ete = 10,
Automne = 20,
Hiver = 30
}
Console
On peut afficher du texte sur le terminal qui a lancé le programme
Console.WriteLine("mon texte");
Console.WriteLine("maVariable vaut : " + maVariable);
// Plus simple, en mettant un $ devant le " on peux insérer les variables entre {}
Console.WriteLine($"maVariable vaut : {maVariable}");
Structure de donnée
En C# il existe plusieurs types de structure de donnée. Les plus courantes sont les classes, les structs, les records et les types anonymes.
Elles ont chacune leur utilité et leur spécificité.
Classes
Les classes sont des types de référence, ce qui signifie que lorsqu’une instance d’une classe est assignée à une nouvelle variable ou passée en argument à une méthode, c’est la référence (adresse mémoire) de l’objet qui est copiée, et non l’objet lui-même. Cela permet de modifier l’objet original via plusieurs références. Les classes sont idéales pour des objets complexes qui nécessitent une gestion de l’héritage, de la polymorphie et de l’encapsulation.
Structs
Les structs sont des types valeur, ce qui signifie qu’une copie complète de la structure est effectuée lorsque vous la passez à une méthode ou l’assignez à une nouvelle variable. Elles sont plus légères que les classes et sont souvent utilisées pour des objets contenant de petites quantités de données immuables, comme les points dans un espace 2D ou des types numériques complexes. L’utilisation des structs peut améliorer les performances dans certaines situations, notamment lorsqu’on veut éviter l’impact des allocations de mémoire sur le tas (heap).
Records
Les records sont des types de référence introduits dans C# 9, conçus pour les objets immuables (par défaut). Ils sont particulièrement utiles pour représenter des objets de données, comme des objets DTO (Data Transfer Objects) ou des objets à usage principalement déclaratif, où la principale fonction de l’objet est de contenir des données. Ils permettent également de bénéficier d’une prise en charge intégrée pour l’égalité structurelle et la comparaison, facilitant leur usage dans des scénarios de traitement de données.
Types Anonymes
Les types anonymes permettent de créer des objets sans définir explicitement une classe ou une structure. Ils sont utiles dans des situations où vous avez besoin de grouper temporairement des données, par exemple dans des requêtes LINQ. Cependant, les types anonymes ne sont généralement utilisés que dans un contexte local, car ils ne peuvent pas être retournés ou passés à travers les frontières d’une méthode de manière pratique.
Collections
En plus de ces types, C# propose des structures de données plus complexes sous forme de collections génériques, telles que List
Les classes
C# étant un langage objet, il permet de créer des classes.
Classe
Une classe est un modèle pour créer des objets. Elle peut contenir des champs, des propriétés, des méthodes, des événements, des indexeurs, des opérateurs, des constructeurs et des destructeurs.
public class MaClasse
{
int _valeur;
public MaClasse(int valeur) {
_valeur = valeur
}
}
// Pour l'instancier
MaClasse c = new MaClasse(0);
Constructeur
Une classe peut avoir un ou plusieurs constructeurs. Si aucun constructeur n’est implémenté, un constructeur par défaut (vide) sera automatiquement fourni.
public class MaClasse
{
int _valeur;
public MaClasse(int valeur) {
_valeur = valeur;
}
public MaClasse() {
_valeur = 0;
}
public MaClasse(int valeur) {
_valeur = valeur;
}
// Constructeur pour EF Core
private MaClasse() {}
}
Depuis la version C# 12 il est possible de définir le constructeur directement après le nom de la classe :
public class MaClasse(int valeur)
{
int _valeur = valeur;
}
Destructeur
Il est possible de définir un destructeur pour une classe. Celui-ci est appelé lorsque l’instance de la classe est détruite.
public class MaClasse
{
~MaClasse()
{
Console.WriteLine("Destruction de l'instance");
}
}
Surcharge / Override
Il est possible de surcharger des méthodes. Surcharger une méthode permet de redéfinir son comportement.
Pour surcharger une méthode, il faut qu’elle soit déclarée dans la classe parente avec le mot clé virtual.
Toutes les classes C# héritent de la classe Object qui contient trois méthodes virtuelles principales :
ToString: Permet de retourner une chaine de caractères représentant l’objet.Equals: Permet de comparer deux objets.GetHashCode: Permet de retourner un code de hachage pour l’objet.
toString
Lorsque vous affichez une instance d’une classe dans la console, elle vous renvoie par défaut le nom complet de la classe : Namespace.Classe.
Voici a quoi ressemble la méthode ToString de la classe Object.
public virtual string? ToString() => this.GetType().ToString();
Vous pouvez surcharger la méthode toString pour choisir quoi afficher.
public class User
{
public int Id { get; set; }
public string? Name { get; set;}
public string? Email { get; set;}
public string? PasswordHash { get; set; }
// Notez la présence de override qui précise que cette méthode existe deja et que l'on la surcharge
public override string ToString()
{
return $"Id: ${Id} Name: ${Name} Email : ${Email} Pass: ${PasswordHash}";
}
}
User u = new User():
u.Id = 0;
u.Name = "abc";
u.Email = "a@b.c";
u.PasswordHash = "";
Console.WriteLine(u);
// Affichera
// Id: 0 Name: abc Email : a@b.c Pass:
C’est très pratique pour déboguer le contenu d’une classe.
Constructeur primaire
Depuis la version 12 de C#, il est possible de définir un constructeur primaire.
Ce constructeur permet de définir les propriétés de la classe directement dans la déclaration de celle-ci.
public class User(int id, string name, string email, string passwordHash)
{
public int Id { get; set; } = id;
public string Name { get; set; } = name;
public string Email { get; set; } = email;
public string PasswordHash { get; set; } = passwordHash;
}
Vous pouvez également combiner un constructeur primaire avec un constructeur classique.
Lorsque vous utilisez un constructeur primaire, vous pouvez l’appeler dans un autre constructeur en utilisant le mot clé this pour appeler le constructeur primaire.
public class User(int id, string name, string email, string passwordHash)
{
public int Id { get; set; } = id;
public string Name { get; set; } = name;
public string Email { get; set; } = email;
public string PasswordHash { get; set; } = passwordHash;
public User(string name, string email, string passwordHash): this(0, name, email, passwordHash) {}
}
Struct
Une structure est un type de données qui permet de regrouper des données de types différents. Elle est similaire à une classe, mais avec quelques différences.
La principale différence entre une structure et une classe est que les structures sont des types de valeur et les classes sont des types de référence.
Cela implique que quand on envoie une structure en paramètre à une méthode, une copie de la structure est envoyée. Alors que pour une classe, c’est une référence qui est envoyée.
Un struct est déclaré avec le mot clé struct.
public struct Personne
{
public string Nom;
public int Age;
}
Il est possible de définir un constructeur pour une structure comme pour une classe.
public struct Personne
{
public string Nom;
public int Age;
public Personne(string nom, int age)
{
Nom = nom;
Age = age;
}
}
Il est possible de définir des méthodes dans une structure.
public struct Personne
{
public string Nom;
public int Age;
public Personne(string nom, int age)
{
Nom = nom;
Age = age;
}
public void Afficher()
{
Console.WriteLine($"Nom: {Nom}, Age: {Age}");
}
}
Record
Les records sont des classes immuables qui permettent de définir des objets de données.
Déclaration
Un record est déclaré avec le mot clé record.
public record Personne(string Nom, int Age);
Personne p = new Personne("Jean", 25);
ou comme ceci, même si on préfère la première méthode
public record Personne
{
public string Nom { get; init; }
public int Age { get; init; }
}
vous pouvez ajouter des méthodes dans un record.
Personne(string Nom, int Age)
{
public void Afficher()
{
Console.WriteLine($"Nom: {Nom}, Age: {Age}");
}
}
Anonymous
Les types anonymes permettent de créer des objets sans définir de classe. Ils sont utiles pour les retours de méthodes ou pour les objets temporaires.
var personne = new { Nom = "Alice", Age = 25 };
Console.WriteLine(personne.Nom); // Alice
Collections
En plus de ces types, C# propose des structures de données plus complexes sous forme de collections génériques, telles que List<T>, Dictionary<TKey, TValue>, HashSet<T>, et bien d’autres.
Ces collections permettent de stocker et de gérer efficacement des ensembles de données avec différents mécanismes d’accès et de manipulation, tels que l’indexation ou la recherche rapide.
Voici quelques-unes des collections les plus couramment utilisées en C# :
List
La classe List<T> est une collection générique qui permet de stocker une liste d’éléments de type T. Elle fournit des méthodes pour ajouter, supprimer, rechercher et trier des éléments, ainsi que pour effectuer d’autres opérations courantes sur les listes.
List<int> nombres = new List<int>();
nombres.Add(1);
nombres.Remove(1);
var number = nombres.Find(x => x == 1);
nombres.Sort((a,b) => a.CompareTo(b));
nombres.Reverse();
foreach (var num in nombres)
{
Console.WriteLine(num);
}
Dictionary<TKey, TValue>
La classe Dictionary<TKey, TValue> est une collection générique qui permet de stocker des paires clé-valeur. Chaque élément du dictionnaire est une paire clé-valeur, où la clé est unique et est utilisée pour accéder à la valeur associée.
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 25);
ages["Bob"] = 30;
ages.Remove("Alice");
foreach (var kvp in ages)
{
Console.WriteLine($"{kvp.Key}: {kvp.Value}");
}
HashSet
La classe HashSet<T> est une collection générique qui permet de stocker un ensemble d’éléments uniques. Elle fournit des méthodes pour ajouter, supprimer et tester la présence d’éléments dans l’ensemble.
HashSet<int> nombres = new HashSet<int>();
nombres.Add(1);
nombres.Add(2);
nombres.Remove(1);
bool contains = nombres.Contains(2);
foreach (var num in nombres)
{
Console.WriteLine(num);
}
Queue
La classe Queue<T> est une collection générique qui implémente une file d’attente (FIFO - First In First Out). Elle fournit des méthodes pour ajouter des éléments à la fin de la file, supprimer des éléments du début de la file, et accéder à l’élément en tête de file.
Queue<string> files = new Queue<string>();
files.Enqueue("Document1.txt");
files.Enqueue("Document2.txt");
string firstFile = files.Dequeue();
string nextFile = files.Peek();
Stack
La classe Stack<T> est une collection générique qui implémente une pile (LIFO - Last In First Out). Elle fournit des méthodes pour ajouter des éléments au sommet de la pile, supprimer des éléments du sommet de la pile, et accéder à l’élément au sommet de la pile.
Stack<string> pile = new Stack<string>();
pile.Push("Document1.txt");
pile.Push("Document2.txt");
string topFile = pile.Pop();
string nextFile = pile.Peek();
Propriété
Les propriétés (properties en anglais) permettent d’encapsuler les champs d’une classe. Elles permettent de contrôler l’accès aux champs et de les protéger.
Sans propriété, il est possible de modifier directement les champs d’une classe. Cela peut être dangereux car il est possible de modifier un champ sans vérifier les valeurs.
Voila un exemple de classe sans propriété:
public class Personne
{
public string Nom;
public int Age;
}
Il est possible de modifier directement les champs Nom et Age sans vérifier les valeurs.
Personne p = new Personne();
p.Nom = "Jean";
p.Age = -5;
Pour éviter cela, il est possible d’utiliser des propriétés.
public class Personne
{
private string _nom;
private int _age;
public string Nom
{
get { return _nom; }
set { _nom = value; }
}
public int Age
{
get { return _age; }
set { _age = value; }
}
}
Il est maintenant impossible de modifier directement les champs Nom et Age. Il est nécessaire de passer par les propriétés.
Personne p = new Personne();
p.Nom = "Jean"; // OK
p.Age = -5; // OK
Il est possible de modifier les propriétés pour ajouter des vérifications.
public class Personne
{
private string _nom;
private int _age;
public string Nom
{
get { return _nom; }
set
{
if (value.Length > 0)
{
_nom = value;
}
}
}
public int Age
{
get { return _age; }
set
{
if (value > 0)
{
_age = value;
}
}
}
}
Il est maintenant impossible de modifier le nom avec une chaine vide ou l’age avec une valeur négative.
Personne p = new Personne();
p.Nom = ""; // KO
p.Age = -5; // KO
Il est possible de mettre les getter et setter en private
public class Personne
{
public string Nom { get; private set; }
public int Age { private get; set; }
}
Il est maintenant impossible de modifier le nom en dehors de la classe. Il est également impossible de lire l’age en dehors de la classe.
Personne p = new Personne();
p.Nom = "Jean"; // KO
Console.WriteLine(p.Age); // KO
Il existe également une variante pour le set qui est init
Dans ce cas, il est possible de modifier la valeur uniquement dans le constructeur.
public class Personne
{
public string Nom { get; init; }
public int Age { get; init; }
}
Il est maintenant impossible de modifier le nom et l’age en dehors du constructeur.
Personne p = new Personne { Nom = "Jean", Age = 25 }; // OK
p.Nom = "Jean"; // KO
p.Age = 25; // KO
Méthode
Une méthode est un bloc de code qui effectue une tâche spécifique. En C#, les méthodes sont définies dans des classes et peuvent être appelées pour exécuter leur code.
Une fonction dans une classe == méthode.
Déclaration
Comme une fonction C#, une méthode est déclarée avec un type de retour, un nom et une liste d’arguments.
public int Addition(int a, int b)
{
return a + b;
}
A la différence des fonctions C#, les méthodes peuvent accéder à l’instance de la classe via le mot clé this.
Elles peuvent également accéder à des champs et des propriétés de la classe.
public class Calculatrice
{
private int _resultat;
private int osef;
public void Addition(int a)
{
_resultat += a;
}
public void Soustraction(int a)
{
_resultat -= a;
}
public int Resultat()
{
return ._resultat;
}
private Calculatrice(int resultat, int osef)
{
_resultat = resultat;
// Comme `osef` est un champ de la classe et un argument de la méthode, il faut utiliser `this` pour accéder au champ de la classe.
// Car par défaut, `osef` fait référence à l'argument de la méthode qui est prioritaire.
this.osef = osef;
}
}
Visibilité
La visibilité d’une classe, d’une méthode ou d’une propriété permet de définir si un élément est accessible depuis l’extérieur de la classe. Il existe plusieurs niveaux de visibilité en C#.
Niveaux de visibilité
public: accessible depuis n’importe où.private: accessible uniquement depuis la classe.protected: accessible depuis la classe et les classes dérivées.
Exemple
public class Personne
{
private string _nom;
private int _age;
public string Nom
{
get { return _nom; }
set
{
if (value.Length > 0)
{
_nom = value;
}
}
}
public int Age
{
get { return _age; }
set
{
if (value > 0)
{
_age = value;
}
}
}
protected void Afficher()
{
Console.WriteLine($"Nom: {Nom}, Age: {Age}");
}
}
Main
Tout programme qui se lance a besoin d’un point d’entrée.
Par défaut le point d’entrée en C# est la fonction Main.
Cette fonction doit être déclaré dans une classe et une seule
class Programme
{
static void Main(string[] args)
{
// Le programme commence ici
Console.WriteLine("Hello, World!");
}
}
Sauf pour les consoles
Si votre projet est un projet console, vous pouvez ne pas utiliser Main.
Vous pouvez écrire votre code directement dans le fichier Program.cs et le compilateur s’occupera de créer le Main pour vous
L’exemple précédent s’écrirai donc simplement comme ceci.
Console.WriteLine("Hello, World!");
Structure
Un programme C# se compose d’un ou plusieurs fichiers, chacun de ces fichier peux contenir :
- Un ou plusieurs namespace
- Une ou plusieurs classe/struct/enum
- Une lise d’import (
using)
Voici un exemple basique
using System; // Importe les définitions du namespace System
namespace MonNamespace;
class MaClasse
{
}
class Programme
{
static void Main(string[] args)
{
// Le programme commence ici
Console.WriteLine("Hello, World!");
}
}
Lambda
Une lambda est une fonction anonyme. Les lambdas sont souvent utilisées pour définir des expressions de fonction qui sont passées comme arguments à des méthodes. Les lambdas peuvent capturer des variables locales et des paramètres de méthode.
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25
Func<int, int, bool> testForEquality = (x, y) => x == y;
Console.WriteLine(testForEquality(5, 5)); // True
Action<string> greet = name =>
{
string greeting = $"Hello {name}!";
Console.WriteLine(greeting);
};
greet("World"); // Hello World!
LINQ
LINQ est un acronyme pour Language Integrated Query. Il s’agit d’une extension du langage C# qui permet de manipuler des données de manière plus simple et plus lisible. LINQ permet de requêter des données de différentes sources (tableaux, listes, bases de données, etc.) en utilisant une syntaxe similaire à SQL.
Syntaxe
La syntaxe de base de LINQ est la suivante :
Prenons uen classe Confiture avec un champ Name et Year :
public class Confiture
{
public string Name { get; set; }
public int Year { get; set; }
}
Insérons quelques données dans une liste :
List<Confiture> confitures = new List<Confiture>
{
new Confiture { Name = "Fraise", Year = 2020 },
new Confiture { Name = "Fraise", Year = 2021 },
new Confiture { Name = "Abricot", Year = 2021 },
new Confiture { Name = "Mûre", Year = 2023 },
new Confiture { Name = "Framboise", Year = 2021 },
new Confiture { Name = "Framboise", Year = 2024 }
};
Si l’on souhaite récupérer les confitures de fraise :
List<Confiture> confitureDeFraises = confitures
.Where(c => c.Name == "Fraise")
.ToList();
Si l’on souhaite récupérer les confitures de fraise de 2021 :
List<Confiture> confitureDeFraises2021 = confitures
.Where(c => c.Name == "Fraise" && c.Year == 2021)
.ToList();
// ou en moins optimisé
List<Confiture> confitureDeFraises2021 = confitures
.Where(c => c.Name == "Fraise")
.Where(c => c.Year == 2021)
.ToList();
Si l’on souhaite récupérer les confitures triées par année :
List<Confiture> confituresTriees = confitures
.OrderBy(c => c.Year)
.ToList();
On utilise Select pour sélectionner une propriété spécifique :
// Récupérer les noms des confitures
List<string> nomsConfitures = confitures
.Select(c => c.Name)
.ToList();
On peut combiner les méthodes :
// Récupérer les noms des confitures de fraise triées par année
string[] nomsConfituresFraise = confitures
.Where(c => c.Name == "Fraise")
.OrderBy(c => c.Year)
.Select(c => c.Name)
.ToArray();
async et await en C#
Introduction
En C#, la programmation asynchrone permet d’effectuer des opérations de longue durée, comme les appels réseau ou l’accès à des base de données, sans bloquer le thread principal. Cela améliore la réactivité des applications en leur permettant de continuer à répondre aux interactions pendant l’exécution de ces tâches.
Le langage C# fournit deux mots-clés importants pour gérer les opérations asynchrones : async et await.
Le Mot-Clé async
Le mot-clé async est utilisé pour marquer une méthode comme asynchrone. Cela signifie que la méthode peut contenir des opérations asynchrones qui ne bloqueront pas le thread sur lequel elles s’exécutent. Une méthode marquée async doit retourner un type Task, Task<T>, ou void.
Exemple d’une Méthode async
public async Task<string> FetchDataAsync()
{
// Simulation d'un appel réseau asynchrone
await Task.Delay(2000); // Simule une attente de 2 secondes
return "Données récupérées";
}
Dans cet exemple, FetchDataAsync est une méthode asynchrone qui retourne un Task<string>. La méthode utilise await pour attendre la fin de Task.Delay(2000), qui simule un délai de 2 secondes.
Le Mot-Clé await
Le mot-clé await est utilisé pour indiquer qu’une méthode asynchrone doit attendre la fin d’une opération asynchrone avant de continuer. Lorsqu’une opération asynchrone est “awaitée”, le contrôle est temporairement retourné au thread appelant, ce qui permet à l’application de rester réactive.
Exemple d’utilisation de await
Copier le code
public async Task ProcessDataAsync()
{
string data = await FetchDataAsync(); // Attend la fin de FetchDataAsync
Console.WriteLine(data); // Affiche "Données récupérées"
}
Dans cet exemple, ProcessDataAsync appelle la méthode FetchDataAsync et utilise await pour attendre son résultat. Pendant que FetchDataAsync est en cours d’exécution, ProcessDataAsync ne bloque pas le thread principal et attendra que la tâche soit terminée pour afficher les données.
Pourquoi Utiliser async et await ?
L’utilisation de async et await permet de simplifier le code asynchrone. Plutôt que de gérer manuellement des callbacks ou des threads, vous pouvez écrire du code asynchrone qui ressemble à du code synchrone classique. Cela rend le code plus lisible, plus facile à écrire, et réduit les risques d’erreurs.
Les exceptions
Les exceptions sont des erreurs qui surviennent lors de l’exécution d’un programme.
Quand on ne souhaite pas traiter une exception, on peut la laisser remonter jusqu’à la méthode appelante.
On utilise pour ca le mot clé throw.
Instruction throw
Pour lancer une exception on utilise le mot clé throw de cette manière.
throw new Exception("Erreur");
On notera l’utilisation de la classe Exception qui prend en paramètre un message d’erreur.
Instruction try-catch
Pour empecher une exception de remonter jusqu’à la méthode appelante, on utilise le bloc try-catch.
De cette manière on peut traiter l’exception et éviter que le programme ne s’arrête.
try
{
throw new Exception("Erreur");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Différents types d’exceptions
Il existe plusieurs types d’exceptions.
Chaque type d’exception hérite de la classe Exception.
On peux créer nos propres exceptions en héritant de la classe Exception.
class MaPropreException : Exception
{
public MaPropreException(string message) : base(message) { }
}
Traiter plusieurs exceptions
On peut les traiter de manière spécifique.
Ici on traite l’exception MaPropreException avant l’exception générique Exception.
Si l’exception envoyé par codeQuiPeutLancerUneException est de type MaPropreException alors le premier bloc catch sera exécuté.
Sinon le deuxième bloc catch sera exécuté.
try
{
codeQuiPeutLancerUneException();
}
catch (MaPropreException e)
{
Console.WriteLine($"Erreur spécifique : ${e.Message}");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Qu’est-ce que le JSON ?
JSON (JavaScript Object Notation) est un format léger de structuration des données. Il est largement utilisé pour échanger des données entre un serveur et un client, ou entre différentes couches d’une application. Le JSON est facile à lire et à écrire pour les humains, et simple à analyser et à générer pour les machines.
Exemple de JSON
{
"nom": "Alice",
"âge": 25,
"compétences": ["C#", "ASP.NET", "Blazor"],
"estActif": true
}
Dans cet exemple, les données décrivent une personne avec un nom, un âge, une liste de compétences, et un statut d’activité.
Qu’est-ce que la Sérialisation ?
La sérialisation est le processus de conversion d’un objet en une représentation textuelle ou binaire qui peut être facilement stockée ou transmise. En C#, la sérialisation en JSON permet de convertir un objet C# en une chaîne JSON. À l’inverse, la désérialisation est le processus de conversion d’une chaîne JSON en un objet C#.
C# vers JSON
Pour sérialiser un objet en JSON en C#, vous pouvez utiliser la classe JsonSerializer fournie par le namespace System.Text.Json.
using System;
using System.Text.Json;
public class Personne
{
public string Nom { get; set; }
public int Âge { get; set; }
public string[] Compétences { get; set; }
public bool EstActif { get; set; }
}
public class Exemple
{
public static void Main()
{
var personne = new Personne
{
Nom = "Alice",
Âge = 25,
Compétences = new[] { "C#", "ASP.NET", "Blazor" },
EstActif = true
};
string jsonString = JsonSerializer.Serialize(personne);
Console.WriteLine(jsonString);
}
}
Résultat:
{"Nom":"Alice","Âge":25,"Compétences":["C#","ASP.NET","Blazor"],"EstActif":true}
JSON vers C#
La désérialisation est le processus inverse de la sérialisation. Elle convertit une chaîne JSON en un objet C#. Cela est particulièrement utile pour recevoir des données JSON d’une API et les convertir en objets manipulables dans votre code.
using System;
using System.Text.Json;
public class Personne
{
public string Nom { get; set; }
public int Âge { get; set; }
public string[] Compétences { get; set; }
public bool EstActif { get; set; }
}
public class Exemple
{
public static void Main()
{
string jsonString = "{\"Nom\":\"Alice\",\"Âge\":25,\"Compétences\":[\"C#\",\"ASP.NET\",\"Blazor\"],\"EstActif\":true}";
// TODO read from file
Personne personne = JsonSerializer.Deserialize<Personne>(jsonString);
Console.WriteLine($"Nom: {personne.Nom}, Âge: {personne.Âge}, Est Actif: {personne.EstActif}");
}
}
Résultat:
Nom: Alice, Âge: 25, Est Actif: True
Requête HTTP
Pour faire communiquer nos services, on utilise des requêtes HTTP.
Emmetre des requêtes HTTP
Voici quelques exemples de requêtes.
using System.Net;
HttpClient client = new HttpClient();
Todo todo = new Todo() { Text = "text", Status = false };
int UserId = 10;
// Requete GET sans paramètre
HttpResponseMessage response = await client.GetAsync("http://localhost:5000/api/Todo/list/");
// Requete GET avec paramètre
HttpResponseMessage response = await client.GetAsync($"http://localhost:5000/api/Todo/list/{UserId}");
// Requete POST avec donnée
HttpResponseMessage response = await client.PostAsJsonAsync($"api/Todo/create/", todo);
// Requete POST avec paramètre et donnée
HttpResponseMessage response = await client.PostAsJsonAsync($"api/Todo/create/{UserId}", todo);
// Requete POST avec paramètre et sans donnée
HttpResponseMessage response = await client.PostAsync($"api/Todo/create/{UserId}");
// Autre méthode (PUT et DELETE)
HttpResponseMessage response = await client.PutAsync($"api/Todo/create/");
HttpResponseMessage response = await client.DeleteAsync($"api/Todo/create/");
HttpResponseMessage response = await client.PutAsJsonAsync($"api/Todo/create/", todo);
Utiliser la réponse
On sait comment envoyer une requêtes, maintenant récupéront sont résultat.
// Prenons comme exemple la récéption d'une classe UserLogin
public class UserLogin
{
public required string Name { get; set; }
public required string Pass { get; set; }
}
// On emet notre requete
HttpResponseMessage response = await client.GetAsync("http://localhost:5000/api/User/login");
// On recupere le résultat et on le transforme en une instance de UserLogin
UserLogin userLogin = await response.Content.ReadFromJsonAsync<UserLogin>();
// Si l'on veux récuperer le texte renvoyé et ne pas le convertir en instance d'une classe on le fait de la manière suivante
string str = await response.Content.ReadAsStringAsync();
Gestion des erreurs
Pour gérer les erreurs, on peut utiliser les exceptions.
try
{
HttpResponseMessage response = await client.GetAsync("http://localhost:5000/api/User/login");
response.EnsureSuccessStatusCode(); // Lève une exception si le code de retour n'est pas entre 200-299
}
catch (HttpRequestException e)
{
Console.WriteLine($"Message : {e.Message}");
}
Headers
Pour ajouter des headers à une requête, on utilise la propriété DefaultRequestHeaders de HttpClient.
Par exemple pour ajouter un token JWT à une requête :
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
ASP.NET
Création d’un projet API
Pour créer un projet Blazor, vous pouvez utiliser la commande suivante:
dotnet new webapi --use-controllers --use-program-main --no-https -n NomDuProjet
On ajoute les options
--no-https pour ne pas utiliser HTTPS
--use-program-main pour utiliser la fonction Main comme point d’entrée.
-n NomDuProjet permet de donner un nom à notre projet, vous pouvez le changer si vous le souhaitez.
--use-controllers pour utiliser les controllers au lieu des API minimales.
Structure d’un projet Web API
Pour créer un projet Web API, il faut d’abord créer un projet .NET Core. Pour cela, on peut utiliser la commande suivante :
dotnet new webapi --use-controllers --use-program-main --no-https --name ConfitureApi
Cette commande va créer un projet Web API nommé ConfitureApi avec les options suivantes :
--use-controllers: Crée un contrôleur par défaut.--use-program-main: Utilise la classeProgramet la fonctionMainpour démarrer l’application.--no-https: Désactive le support HTTPS.--name ConfitureApi: Donne le nomConfitureApiau projet.
Structure du projet
Voici la structure de fichiers et de dossiers générée par la commande précédente :
ConfitureApi/
├── Controllers/
│ └── WeatherForecastController.cs
├── Properties/
│ └── launchSettings.json
├── appsettings.json
├── ConfitureApi.csproj
├── Program.cs
└── WeatherForecast.cs
Controllers/: Dossier contenant les contrôleurs de l’API. C’est ici que l’on définit les routes et les actions.Controllers/WeatherForecastController.cs: Contrôleur par défaut généré par la commande.Properties/: Dossier contenant les fichiers de configuration.appsettings.json: Fichier de configuration de l’application.ConfitureApi.csproj: Fichier de configuration du projet.Program.cs: ClasseProgramcontenant la fonctionMainpour démarrer l’application.WeatherForecast.cs: Modèle de données pour les prévisions météo.
On utilise généralement :
- le dossier
Controllerspour stocker les contrôleurs de l’API - le dossier
Models(pas crée automatiquement)pour les modèles de données, - le dossier
Servicespour les services utilisés par l’API. - le dossier
Migrationspour les migrations de base de données.
Contrôleur par défaut
Le contrôleur WeatherForecastController généré par défaut contient une action Get qui renvoie des prévisions météo aléatoires.
Controlleur
Un controlleur est une classe qui hérite de ControllerBase.
Cette classe est précédé par 2 annotations
// Indique que notre controller sera accessible par l'url api/LeNomDuController
[Route("api/[controller]")]
// Indique que cette classe est un controleur d'API
[ApiController]
Annotations
Dans notre classe on défini des méthodes qui seront accessible depuis une route HTTP.
Cela se fait simplement en ajoutant une annotation avant la méthode. Voici différents exemples d’annotations possible.
// Annotation basique, écoute sur la même URL que le controlleur
// filtre selon la méthode de la requtête HTTP GET/POST/PUT/DELETE
[HttpGet]
[HttpPost]
[HttpPut]
[HttpDelete]
// On peux surcharger l'URL a laquel la méthode sera appelé
// Ici pour appeler cette méthode on contactera /api/Controller/a/b/c
[HttpGet("a/b/c")]
// On peux également récuperer des paramètre passé dans l'url
// Ici on déclare que dans notre url on a un paramètre id
// On retrouve ce paramètre dans les arguments de notre méthode
[HttpGet("a/{id}")]
public void Param(int id) {}
Envoyer de la donnée
Dans l’exemple précédent on a vu que l’on pouvait passer des paramètres dans l’URL. Cela est cependant peux adapter quand notre volume de donnée a transmettre est important.
Dans ce cas la on utilise en général les méthode POST ou PUT qui servent a transmettre plus d’informations.
[HttpPost]
public void POST(Data data)
{
Console.WriteLine(data.ChampLong);
}
// On peux même combiner les 2 en passant par url et par donnée
[HttpPost("{id}")]
public void POST(int id, Data data)
{
Console.WriteLine(id);
Console.WriteLine(data.ChampLong);
}
public class Data
{
public string ChampLong { get; set; }
public int[] TableauDeInt { get; set; }
}
Exemple
Dans l’exemple qui suit on définit un controller Random qui sera joignable sur /api/Random.
Il expose 3 méthode qui sont appelable sur les URLs suivante :
- GET /api/Random
- GET /api/Random/0/100 0 = min 100 = max
- POST /api/Random data={ “min”: 0, “max”: 100 }
using Microsoft.AspNetCore.Mvc;
namespace MonApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class Random : ControllerBase
{
// GET: api/Random
[HttpGet]
public int RandomGet()
{
return new Random().Next();
}
// GET api/Random/0/100
[HttpGet("{min}/{max}")]
public int RandomGetMinMax(int min, int max)
{
return new Random().Next(min, max);
}
// POST api/Random/post DATA JSON { "min": 0, "max": 100 }
[HttpPost]
public int RandomPost(RandomValue value)
{
return new Random().Next(value.Min, value.Max);
}
}
public class RandomValue
{
public int Min { get; set; }
public int Max { get; set; }
}
}
Code de retour
Code de retour
Une réponse HTTP contient un code de retour. Ce code permet de savoir si la requête a été traité correctement ou non.
Vous connaissez très probablement le 404 - Not Found.
IActionResult
En ASP.Net le type de retour s’exprime avec une classe ActionResult qui implémente l’interface IActionResult.
Il en existe plusieurs qui hérite de ActionResult.
En voici quelques unes :
return Ok(value); // 200 - OK tout s'est bien passé, on renvoi la valeur a l'appelant
return CreatedAtAction(nameof(Getter), new { id = value.Id }, value); // 201 - Le résultat a bien été crée
return NoContent("J'ai rien"); // 204
return BadRequest("C'est pas valide"); // 400
return NotFound("Erreur: j'ai pas trouvé"); // 404
return StatusCode(500, "Erreur interne"); // 500
La dernière méthode StatusCode permet de renvoyer un code HTTP personnalisé.
Exemple d’usage
Une méthode qui renvoi un objet Confiture si elle existe dans la base de donnée.
On peux voir que le type de retour est ActionResult<Confiture> ce qui signifie que l’on renvoi un objet de type Confiture avec un code 200 ou un code d’erreur.
Ici on renvoi un code 404 si la confiture n’existe pas dans la base.
[HttpGet("{id}")]
public async ActionResult<Confiture> GetConfiture(int id)
{
// TODO enleveer db
var confiture = _context.Confitures.Find(id);
if (confiture == null)
{
return NotFound(); // 404
}
return Ok(confiture); // 200
}
Par défaut le code de retour est 200 - OK si on ne spécifie pas de code de retour.
Les deux lignes suivantes sont équivalentes.
return Ok(confiture);
return confiture;
Scalar
Scalar fournir une interface web pour tester notre service.
On l’active en installant une package Nuget.
Pour se faire lancer cette commande dans le dossier de votre projet
dotnet add package Scalar.AspNetCore
using Scalar.AspNetCore; // Ajoutez ce using
public class Program
{
public static void Main(string[] args)
{
...
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(); // Ajoutez cette ligne
}
...
}
}
Puis dans le Program.cs
Pour lancer automatiquement scalar on modifie le fichier Properties/launchSettings.json.
On passera launchBrowser à true et on ajoutera launchUrl à http://localhost:5100/scalar/v1
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5100",
"launchUrl": "http://localhost:5100/scalar/v1",
...
}
}
}
Injection de dépendance
Comme on utilise le framework ASP.Net on ne contrôle pas l’instanciation de nos Controller.
Si j’ai besoin d’avoir accès a un Client HTTP ou a une classe particulière dans mon controller, j’utilise l’injection de dépendance.
Dans mon Program.cs je peux demander de au framework de créer certaine classe pour moi.
// Permet au framework d'injecter une instance de MaClasse dans les controller
// Scoped signifie qu'a chaque requête l'instance est recrée
builder.Services.AddScoped<MaClasse>();
// Si je veux avoir un instance persistante je peux demander un singleton
builder.Services.AddSingleton<MaClasse>();
// Si je veux avoir accès un HTTPClient on peux utiliser la focntion suivante
builder.Services.AddHttpClient();
namespace Exemple.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ExempleController : ControllerBase
{
HttpClient _client;
MaClasse _maClasse;
// Dans mon constructeur je demande un HTTPClient et une instance de MaClasse
// Ceux si seront automatiquement crée par le framework sans action de notre part autre que l'ajout dans le `Program.cs`
public ExempleController(HttpClient client, MaClasse mc)
{
client = client;
_maClasse = mc;
}
}
}
Configuration
Certaine variable de configuration ne doivent pas être stocké dans le code source.
Soit parce qu’elles sont sensible (mot de passe, clé d’API) soit parce qu’elles peuvent changer sans recompiler le code (adresse de la base de donnée, adresse d’un service externe).
Pour cela on va stocker ces variables dans un fichier de configuration.
Ajouter une variable de configuration
Pour ajouter une variable de configuration on va ajouter une section dans le fichier appsettings.json.
{
"ConnectionStrings": {
"Sqlite": "Data Source=Confiture.db"
},
"APIKey": "abcedf"
}
Lire une variable de configuration
Pour lire une variable de configuration on va utiliser la classe Configuration fournie par le framework.
Pour se faire, ajouter un constructeur à votre classe et injecter la classe Configuration qui implémente IConfiguration.
public class RecetteService
{
private readonly string _connectionString;
private readonly string _apiKey;
public RecetteService(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("Sqlite");
_apiKey = configuration["APIKey"];
}
}
Base de données
Quand une application a besoin de stocker puis de retrouver des informations sur la durée elle utilise en général une base de donnée.
Le framework ASP.Net Core nous permet facilement de se connecter a une BDD puis d’y faire des requêtes.
Les base de données supporté
Il existe plusieurs base supporté nottament:
- SQL Server
- Postgres
- MariaDB
- Mysql
- Sqlite
- MongoDB
On peux très facilement se connecter a d’autre base en ajoutant une dépendance via le système Nuget.
Sqlite
Cette base est particulièrement intéressante car elle n’a pas besoin de serveur pour fonctionner. En effet le stockage des information se fait dans un fichier sur le disque.
Cela est très pratique pour effectuer des tests ou pour des petites applications.
BDD en concept objet
Le stockage et la récupération des informations utilise les classes défini dans le projet. Il faudra donc définir une classe qui représente l’objet que l’on souhaite stocker.
Voici un exemple pour ajouter une base Sqlite a un projet.
Pour cet exemple nous gérerons des Confiture
public class Confiture
{
public int Id { get; set; }
public string Fruit { get; set; }
public int Annee { get; set; }
public Confiture(int id, string fruit, int annee)
{
Id = id;
Fruit = fruit;
Annee = annee;
}
}
Maintenant que l’on a défini la structure de notre Confiture passons a la définition de notre BDD.
Instalation des dépendances
Pour pouvoir sauvegarder nos données, nous allons utiliser l’entity framework.
C’est un framework qui s’occupe d’abstraire les requêtes vers la base pour nous.
Pour cela, il faut l’ajouter à notre projet. Nous en profiterons pour ajouter dotnet ef qui permettra d’initialiser notre base de donnée
pour se faire, executer ces commandes dans le répértoire du projet depuis un terminal.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.*
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 9.*
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.*
dotnet tool install --global dotnet-ef --version 9.*
Définir notre connection a une BDD
Pour défini une BDD, il faut créer une classe qui hérite de DbContext.
Le nom de cette nouvelle classe se termine en général par Context.
La classe doit posséder un constructeur.
Puis pour chaque type d’élement à stocker on va créer une variable de type DbSet<Type>
Voila donc a quoi ressemble une classe basique
using Microsoft.EntityFrameworkCore;
namespace ConfitureApi.Models;
public class ConfitureContext : DbContext
{
public ConfitureContext(DbContextOptions<ConfitureContext> options)
: base(options)
{
}
public DbSet<Confiture> Confitures { get; set; } = null!;
}
Configurer Sqlite
Notre base n’est actuellement pas configuré pour utiliser Sqlite.
On va ajouter une méthode OnConfiguring qui sera appelé pour configurer notre connection à la BDD.
Dans cette méthode on va spécifier que l’on désire utiliser Sqlite et on donnera le lien vers le fichier de la base.
public class ConfitureContext : DbContext
{
public ConfitureContext(DbContextOptions<ConfitureContext> options)
: base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// Connexion a la base sqlite
options.UseSqlite("Data Source=Confiture.db");
}
public DbSet<Confiture> Confitures { get; set; } = null!;
}
Initialiser la BDD au lancement
Pour que notre programme se connecte a notre BDD il faut le lui demander.
Cela se passe dans la phase de configuration dans Program.cs
builder.Services.AddDbContext<ConfitureContext>();
Les Migrations
On va créer un fichier de migration qui nous permettra de créer la structure de notre base.
Une migration est un fichier qui permet de faire passer une base d’un état A à un état B mais également de revenir à l’état A en efféctuant les opérations inverse.
Dans notre cas, ce premier fichier de migration va servir a créer notre table Confitures avec ses champs.
La encore, c’est une commande qui va nous aider
dotnet ef migrations add InitialisationDeLaDB
Un nouveau dossier Migrations a du apparaitre. A l’intérieur 3 fichiers dont un qui devrait ressembler a ca :
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ConfitureApi.Migrations
{
/// <inheritdoc />
public partial class Init : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// La liste des opérations à executer pour créer notre table Confitures
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
// Les opération inverse, donc ici la suppression de la table Confitures
}
}
}
Création de la BDD
Notre applicatif connait désormais notre BDD et s’y connecte au démarrage.
Mais elle n’existe pas encore cette BDD !
dotnet ef database update
Cette commande va commencer par créer notre BDD Sqlite si elle n’existe pas.
Puis elle va y appliquer les migrations une par une dans l’ordre.
Dans notre cas, une seul migration sera appliqué, pour créer notre table Confitures.
Utiliser la BDD dans un controlleur
using Microsoft.EntityFrameworkCore;
Pour accéder à notre BDD dans un controlleur il faut injecter notre contexte dans le constructeur. On créer une variable privée pour stocker le contexte et on l’initialise dans le constructeur.
using ConfitureApi.Models;
namespace ConfitureApi.Controllers;
[Route("api/[controller]")]
[ApiController]
public class ConfitureController : ControllerBase
{
private readonly ConfitureContext _context;
public ConfitureController(ConfitureContext ctx)
{
_context = ctx;
}
}
Récupérer des données
Pour récupérer des données de notre BDD on va utiliser la méthode Find de notre contexte.
[HttpGet("{id}")]
public async Task<ActionResult<Confiture>> GetConfiture(int id)
{
// on récupère la confiture correspondant a l'id
var confiture = await _context.Confitures.FindAsync(id);
if (confiture == null)
{
return NotFound();
}
// on retourne la confiture
return Ok(confiture);
}
Ajouter des données
Pour ajouter des données dans notre BDD on va utiliser la méthode Add de notre contexte.
Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.
On peux ensuite retourner un code 201 pour indiquer que la création a bien eu lieu.
class ConfitureCreation
{
public string Fruit { get; set; }
public int Annee { get; set; }
}
[HttpPost]
public async Task<ActionResult<Confiture>> PostConfiture(ConfitureCreation confitureCreation)
{
// on créer une nouvelle confiture avec les informations reçu
Confiture confiture = new Confiture {
Fruit = confitureCreation.Fruit,
Annee = confitureCreation.Annee
};
// on l'ajoute a notre contexte (BDD)
_context.Confitures.Add(confiture);
// on enregistre les modifications dans la BDD ce qui remplira le champ Id de notre objet
await _context.SaveChangesAsync();
// on retourne un code 201 pour indiquer que la création a bien eu lieu
return CreatedAtAction(nameof(GetConfiture), new { id = confiture.Id }, confiture);
}
Mettre a jour des données
Pour mettre a jour des données dans notre BDD on va utiliser la méthode Update de notre contexte.
Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.
On peux ensuite retourner un code 204 pour indiquer que la modification a bien eu lieu.
Le code est un peu plus complexe car il faut vérifier que l’objet que l’on souhaite modifier n’a pas été modifié entre temps.
On utilise donc un try catch pour gérer l’erreur DbUpdateConcurrencyException.
[HttpPut("{id}")]
public async Task<IActionResult<Confiture>> PutConfiture(Confiture confitureUpdate)
{
// on récupère la confiture que l'on souhaite modifier
Confiture confiture = await _context.Confitures.FindAsync(confitureUpdate.Id);
if (confiture == null)
{
return NotFound();
}
// on met a jour les informations de la confiture
confiture.Fruit = confitureUpdate.Fruit;
confiture.Annee = confitureUpdate.Annee;
try
{
// on enregistre les modifications
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// si une erreur de concurrence survient on retourne un code 500
return StatusCode(500, "Erreur de concurrence");
}
// on retourne un code 200 pour indiquer que la modification a bien eu lieu
return Ok(confiture);
}
Supprimer des données
Pour supprimer des données dans notre BDD on va utiliser la méthode Remove de notre contexte.
Puis on va appeler la méthode SaveChanges pour enregistrer les modifications.
On peux ensuite retourner un code 204 pour indiquer que la suppression a bien eu lieu.
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteConfiture(int id)
{
// on récupère la confiture que l'on souhaite supprimer
Confiture confiture = await _context.Confitures.FindAsync(id);
if (confiture == null)
{
return NotFound();
}
// on indique a notre contexte que l'objet a été supprimé
_context.Confitures.Remove(confiture);
// on enregistre les modifications
await _context.SaveChangesAsync();
// on retourne un code 204 pour indiquer que la suppression a bien eu lieu
return NoContent();
}
Conclusion
Les bases de données sont un élément essentiel de toute application.
ASP.Net Core nous permet de facilement nous connecter a une base et d’y effectuer des opérations sans intégrer de logique sépcifique à la base choisi.
Authentification
L’authentification est une étape importante dans le développement d’une application. Elle permet de vérifier l’identité de l’utilisateur et de lui donner accès à certaines ressources. Dans ce chapitre, nous allons voir comment mettre en place un système d’authentification dans une application ASP.NET.
Nous utiliserons JWT (JSON Web Token) pour sécuriser nos API et nos pages web. JWT est un standard ouvert qui permet de créer des jetons d’accès sécurisés et auto-suffisants. Il est basé sur JSON et est facile à utiliser dans différents langages de programmation.
JWT
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version "9.0.*"
Générer un JWT
Pour générer une JWT nous commençons par générer des Claims. Les Claims sont des informations sur l’utilisateur qui sont stockées dans le JWT. Par exemple, nous pouvons stocker l’identifiant de l’utilisateur, son nom, son email, etc.
using System.Security.Claims;
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "8"),
new Claim(ClaimTypes.Name, "Roger"),
new Claim(ClaimTypes.Role, "Admin"),
};
Un claim est composé d’un type et d’une valeur.
Le type et la valeur sont tout les deux des chaînes de caractères.
Il existe plusieurs types de claims prédéfinis dans .NET, utilisez l’enum ClaimTypes pour les utiliser.
Une fois les claim générés, nous allons créer une clé secrète pour signer le JWT. La clé secrète est utilisée pour vérifier l’intégrité du JWT. Elle doit être gardée secrète et ne doit pas être partagée.
using Microsoft.IdentityModel.Tokens;
SymmetricSecurityKey key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("TheSecretKeyThatShouldBeStoredInTheConfiguration")
);
SigningCredentials credentials = new SigningCredentials(
key,
SecurityAlgorithms.HmacSha256
);
Enfin, nous allons créer un token avec les claims et la clé secrète.
using System.IdentityModel.Tokens.Jwt;
JwtSecurityToken token = new JwtSecurityToken(
issuer: "localhost:5000", // Qui émet le token ici c'est notre API
audience: "localhost:5000", // Qui peut utiliser le token ici c'est notre API
claims: claims, // Les informations sur l'utilisateur
expires: DateTime.Now.AddMinutes(3000), // Date d'expiration du token
signingCredentials: credentials // La clé secrète
);
string tokenString = new JwtSecurityTokenHandler().WriteToken(token);
Exemple de JWT
Voici un exemple de JWT.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic3RyaW5nIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJleHAiOjE3Mjk1OTc0NDUsImlzcyI6ImxvY2FsaG9zdDo1MDAwIiwiYXVkIjoibG9jYWxob3N0OjUwMDAifQ.S17n1mLz34r6Aipb_cbrMebDm4AESdqdF1Ge-XEckkI
Je vous invite a aller sur jwt.io pour décoder ce token et voir son contenu.
Valider un JWT
Nous savons désormais générer un JWT, mais comment s’en servir.
Il faut indiquer à notre API que nous voulons utiliser JWT pour sécuriser nos routes. Nous allons donc ajouter un middleware qui va vérifier la validité du JWT.
program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ClockSkew = TimeSpan.FromMinutes(10), // Temps de tolérance pour la date d'expiration
ValidateLifetime = true, // Vérifie la date d'expiration
ValidateIssuerSigningKey = true, // Vérifie la signature
ValidAudience = "localhost:5000", // Qui peut utiliser le token ici c'est notre API
ValidIssuer = "localhost:5000", // Qui émet le token ici c'est notre API
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("TheSecretKeyThatShouldBeStoredInTheConfiguration")
),
RoleClaimType = ClaimTypes.Role // Dans quel claim est stocké le role
};
});
Via ce middleware, notre API va vérifier la validité du JWT à chaque requête. Si le JWT est valide, l’utilisateur pourra accéder à la ressource demandée. Sinon, il recevra une erreur 401.
Authentification
Activation de l’authentification
Maintenant que nous savons générer et valider un JWT, nous allons voir comment l’utiliser pour sécuriser nos routes.
Il faut d’abord activer l’authentification dans notre API.
program.cs
builder.Services.AddAuthorization();
...
app.UseAuthentication();
app.UseAuthorization();
Vérification de l’authentification
Pour vérifier l’authentification d’un utilisateur, nous allons ajouter un attribut [Authorize] sur les contrôleurs ou les actions que nous voulons sécuriser.
Voici un exemple d’utilisation de l’attribut [Authorize].
[Authorize] // Indique que l'on vérifie l'authentification de l'utilisateur dans ce contrôleur
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
[HttpGet]
[AllowAnonymous] // Permet d'accéder à cette route sans être authentifié
public IActionResult Anyone()
{
return Ok("Hello World");
}
[HttpGet]
[Authorize(Roles = "Admin")] // Permet d'accéder à cette route si l'utilisateur a le role Admin
public IActionResult Admin()
{
return Ok("Hello Admin");
}
[HttpGet]
[Authorize(Roles = "Admin, User")] // Permet d'accéder à cette route si l'utilisateur a le role Admin ou User
public IActionResult UserOrAdmin()
{
return Ok("Hello User or Admin");
}
[HttpGet]
[Authorize] // Permet d'accéder à cette route si l'utilisateur est authentifié peux importe son role
public IActionResult Authentified()
{
return Ok("Hello Authentified");
}
}
Récupération de l’utilisateur
Pour récupérer l’utilisateur qui a fait la requête, nous allons utiliser la propriété User de l’objet HttpContext.
Si l’on reprend les claims suivant :
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "8"),
new Claim(ClaimTypes.Name, "Roger"),
new Claim(ClaimTypes.Role, "Admin"),
};
Voici comment récupérer les informations de cet utilisateur quand il fait une requête.
[HttpGet]
[Authorize] // La route est sécurisée
public IActionResult Get()
{
int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value); // 8
string userName = User.FindFirst(ClaimTypes.Name).Value; // Roger
string userRole = User.FindFirst(ClaimTypes.Role).Value; // Admin
return Ok(new
{
Id = userId,
Name = userName,
Role = userRole
});
}
Password Hasher
Dans cette partie nous allons voir comment hasher un mot de passe pour le stocker en base de donnée.
Hasher un mot de passe
Pour hasher un mot de passe, on va utiliser la classe PasswordHasher<T> qui permet de dériver une clé à partir d’un mot de passe.
Prenons un classe User avec un champ Password :
public class User
{
public string Password { get; set; }
}
Pour hasher le mot de passe, on va utiliser la classe PasswordHasher<T> :
using Microsoft.AspNetCore.Identity;
var hasher = new PasswordHasher<User>();
var password = "LePasswordSecret";
var user = new User { Password = "" };
user.Password = hasher.HashPassword(user, password);
On peut maintenant vérifier si le mot de passe est correct :
var result = hasher.VerifyHashedPassword(user, user.Password, password);
if (result == PasswordVerificationResult.Success)
{
Console.WriteLine("Mot de passe correct");
}
else
{
Console.WriteLine("Mot de passe incorrect");
}
CORS
CORS (Cross-Origin Resource Sharing) est un mécanisme qui permet de sécuriser les API en limitant les origines qui peuvent y accéder.
Documentation : https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
Configuration
Pour configurer CORS, nous allons utiliser la classe AddCors dans le fichier program.cs.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder => builder.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
});
Pour utiliser CORS, nous allons ajouter le middleware UseCors dans le fichier program.cs.
app.UseCors("AllowAll");
Cela va permettre à toutes les origines de faire des requêtes vers notre API.
Pour limiter les origines qui peuvent faire des requêtes vers notre API, nous pouvons configurer une politique CORS.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigin", builder => builder.WithOrigins("https://csharp.nouvet.fr/front3"));
});
Cela va permettre que seule l’URL du frontend puisse faire des requêtes vers notre API.
Pour utiliser une politique CORS, nous allons ajouter le middleware UseCors dans le fichier program.cs.
app.UseCors("AllowSpecificOrigin");
SignalR
Pour utiliser SignalR, vous devez ajouter les headers suivants dans la politique CORS.
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.SetIsOriginAllowed(origin => true) // Allow any origin
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials(); // SignalR requires credentials
});
});
Validation des données
Pour valider les données, nous pouvons utiliser les annotations de data annotation.
[Required]
public string Nom { get; set; }
Ici le champ Nom est obligatoire.
Vous pouvez également utiliser les annotations suivantes:
- [Key] : Le champ est la clé primaire de la table
- [Required] : Le champ est obligatoire
- [MinLength(3)] : Le champ doit avoir une longueur minimale de 3 caractères
- [MaxLength(100)] : Le champ doit avoir une longueur maximale de 100 caractères
- [EmailAddress] : Le champ doit être une adresse email valide
- [RegularExpression(@“^[a-zA-Z0-9]+$”)] : Le champ doit comporter uniquement des lettres et des chiffres
Voici un exemple avec la classe Confiture:
public class Confiture
{
[Key]
public int Id { get; set; }
[Required]
[MinLength(3)]
[MaxLength(100)]
public string Fruit { get; set; }
[Required]
[Range(1900, 2025)]
public int Annee { get; set; }
public Confiture(int id, string fruit, int annee)
{
Id = id;
Fruit = fruit;
Annee = annee;
}
}
La liste des annotations de data annotation est disponible ici.
Les Middlewares
Dans ASP.NET Core, le pipeline de traitement des requêtes (Request Pipeline) est composé d’une série de composants appelés Middlewares.
Chaque middleware intercepte la requête HTTP entrant, peut effectuer des opérations avant et après le middleware suivant, et décide de passer ou non la requête au suivant.
Le Pipeline
Imaginez le pipeline comme une série de couches d’oignon ou de filtres.
- La requête arrive.
- Elle traverse le Middleware 1.
- Elle traverse le Middleware 2.
- …
- Elle atteint le Endpoint (votre Controller).
- Le Controller génère une réponse.
- La réponse repasse par le Middleware 2 (en sens inverse).
- La réponse repasse par le Middleware 1.
- La réponse est envoyée au client.

Utilisation standard
Vous utilisez déjà des middlewares sans le savoir dans Program.cs :
var app = builder.Build();
// Middleware de redirection HTTPS
app.UseHttpsRedirection();
// Middleware d'autorisation
app.UseAuthorization();
// Middleware qui mappe les controllers aux routes
app.MapControllers();
app.Run();
L’ordre de déclaration dans Program.cs est CRUCIAL. Si UseAuthorization est placé avant UseAuthentication (qui décode le user), l’autorisation échouera car l’utilisateur ne sera pas encore connu.
Créer un Middleware personnalisé
Pour des besoins spécifiques (Logging global, Gestion d’erreur globale, Headers de sécurité), on peut créer nos propres middlewares.
Un middleware est généralement une classe qui possède :
- Un constructeur prenant un
RequestDelegate(le pointeur vers le middleware suivant). - Une méthode
InvokeAsyncprenant leHttpContext.
public class SimpleLoggerMiddleware
{
private readonly RequestDelegate _next;
public SimpleLoggerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Avant le controller : On log la requête
Console.WriteLine($"Request: {context.Request.Method} {context.Request.Path}");
// On passe la main au middleware suivant
await _next(context);
// Après le controller : On log le status code
Console.WriteLine($"Response: {context.Response.StatusCode}");
}
}
Enregistrement
Pour utiliser ce middleware, on l’ajoute dans Program.cs :
app.UseMiddleware<SimpleLoggerMiddleware>();
Cas d’usage : Gestion globale des erreurs
Plutôt que de mettre des try/catch dans chaque méthode de vos controllers, vous pouvez utiliser un middleware qui englobe toute l’exécution.
Si une exception “remonte” jusqu’au middleware sans être attrapée, le middleware peut l’intercepter, formatter une belle réponse JSON d’erreur, et renvoyer un code HTTP approprié (500, 400, 404).
Cela permet de garder vos controllers propres et focalisés sur la logique métier.
Changer le code de retour et le contenu depuis un middleware
Voici comment fonctionne le code ci-dessous :
Dans un middleware, vous pouvez modifier le code de retour HTTP (StatusCode) et le contenu de la réponse. Par exemple, ici :
context.Response.StatusCode = statusCode;: fixe le code HTTP de réponse (ex : 200, 400, 500, …).context.Response.ContentType = "application/json";: indique que la réponse sera du JSON.await context.Response.WriteAsync(...): écrit le contenu dans la réponse. Ici on utiliseJsonSerializer.Serialize(...)pour transformer un objet .NET en JSON.
Ce modèle permet donc de gérer proprement les erreurs côté serveur :
- On choisit le code de retour HTTP,
- On construit une réponse structurée (souvent avec un message, un code d’erreur, etc.),
- On l’envoie au format JSON à l’utilisateur.
C’est la base de la gestion d’erreur globale par middleware, pour retourner des messages d’erreur uniformisés à toutes les erreurs non attrapées ailleurs.
// On configure les options de sérialisation pour avoir un JSON en camelCase comme dans les contrôleurs
var option = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(
JsonSerializer.Serialize(new MaClasse(1, "abc"), option)
);
Le code ci-dessus est un exemple de comment on peut changer le code de retour et le contenu de la réponse depuis un middleware.
Architecture : Services et Injection de Dépendance
Au début d’un projet ASP.NET Core, on a tendance à écrire toute la logique métier directement dans les méthodes des Contrôleurs. C’est ce qu’on appelle l’anti-pattern “Fat Controller”.
Pourquoi séparer le code ?
Les contrôleurs ont pour unique responsabilité de gérer le protocole HTTP :
- Recevoir la requête (et désérialiser le JSON).
- Valider les entrées basiques.
- Appeler la logique métier.
- Renvoyer une réponse HTTP appropriée (200 OK, 404 Not Found, etc.).
Ils ne devraient JAMAIS contenir :
- La logique de calcul (ex: formule du coût du reset).
- Les appels directs à la base de données (si possible).
- La logique de validation complexe (ex: vérifier si l’utilisateur a assez d’argent).
Cette séparation permet de :
- Tester la logique métier sans lancer un serveur web (Tests Unitaires).
- Réutiliser la logique métier ailleurs (ex: dans une tâche planifiée).
- Lire plus facilement le code.
Création d’un Service
Un service est une classe C# standard qui contient la logique métier.
Exemple : GameService.cs
public class GameService
{
private readonly AppDbContext _context;
public GameService(AppDbContext context)
{
_context = context;
}
public void Click(int userId)
{
var progression = _context.Progressions.FirstOrDefault(p => p.UserId == userId);
if (progression == null)
{
throw new Exception("Progression introuvable");
}
progression.Count += (int)(1 * Math.Pow(1.5, progression.Multiplier));
_context.SaveChanges();
}
}
Injection de Dépendance (DI)
Pour utiliser ce service dans un contrôleur, il faut l’enregistrer dans le conteneur de dépendances.
Dans Program.cs :
// Enregistrement du service
// AddScoped : Une nouvelle instance est créée pour chaque requête HTTP
builder.Services.AddScoped<GameService>();
Ensuite, on l’injecte dans le constructeur du contrôleur :
[ApiController]
[Route("api/[controller]")]
public class GameController : ControllerBase
{
private readonly GameService _gameService;
public GameController(GameService gameService)
{
_gameService = gameService;
}
[HttpPost("click")]
public IActionResult Click()
{
// On récupère l'ID de l'utilisateur (via le token JWT par exemple)
int userId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
try
{
_gameService.Click(userId);
return Ok();
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
Refactoring étape par étape
- Identifiez un bloc de logique dans votre contrôleur.
- Créez une méthode dans un Service correspondant (
UserService,GameService,InventoryService). - Déplacez le code (et les dépendances comme le
DbContext) dans ce Service. - Appelez le Service depuis le Contrôleur.
Votre contrôleur va maigrir et devenir beaucoup plus clair !
Transactions et Concurrence
Dans un environnement web, plusieurs utilisateurs peuvent faire des requêtes simultanément. Pire, un même utilisateur peut envoyer la même requête plusieurs fois très rapidement (double-clic, script, lag).
Cela peut causer des problèmes graves d’intégrité des données, appelés Race Conditions (Conditions de concurrence).
Le problème
Imaginons la fonction “Acheter un objet” :
- Lire le solde de l’utilisateur (ex: 100 pièces).
- Vérifier si le solde >= prix (ex: 50 pièces).
- Déduire le prix (Nouveau solde = 50).
- Sauvegarder le solde.
- Ajouter l’objet à l’inventaire.
Si deux requêtes arrivent exactement en même temps :
- Requête A lit le solde : 100.
- Requête B lit le solde : 100 (car A n’a pas encore sauvegardé !).
- Requête A déduit 50 et sauvegarde -> Solde 50.
- Requête B déduit 50 et sauvegarde -> Solde 50.
- Résultat : L’utilisateur a payé 50 pièces au total mais a reçu l’objet deux fois ! Il aurait dû payer 100.
Solution 1 : Les Transactions
Une transaction garantit que plusieurs opérations de base de données sont traitées comme une seule unité indivisible (Atomicité).
Si une erreur survient au milieu, tout est annulé (Rollback).
Avec Entity Framework Core :
using var transaction = _context.Database.BeginTransactionAsync();
try
{
// Opération 1 : Débit
var user = _context.Users.Find(userId);
user.Money -= 50;
_context.SaveChangesAsync();
// Opération 2 : Ajout Item
_context.Inventory.Add(newItem);
_context.SaveChangesAsync();
// Si on arrive ici sans erreur, on valide tout
await transaction.CommitAsync();
}
catch (Exception)
{
// En cas d'erreur, tout est annulé automatiquement
// (Le Rollback est implicite si on ne commit pas, mais on peut le forcer)
await transaction.RollbackAsync();
throw;
}
Cependant, les transactions seules ne résolvent pas toujours le problème de lecture concurrente (Requête B qui lit 100 alors que A est en train de modifier). Il faut souvent changer le Niveau d’Isolation de la transaction (ex: Serializable), ce qui peut ralentir la base de données.
Solution 2 : Concurrence Optimiste
La concurrence optimiste part du principe que les conflits sont rares. On laisse tout le monde lire, mais au moment d’écrire, on vérifie si la donnée a changé entre temps.
On ajoute souvent un jeton de concurrence (ex: RowVersion ou Timestamp) dans la table.
public class User
{
public int Id { get; set; }
public int Money { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
Lors de l’update :
UPDATE Users SET Money = 50 WHERE Id = 1 AND RowVersion = [AncienneValeur]
Si quelqu’un d’autre a modifié la ligne entre temps, le RowVersion a changé. La clause WHERE ne trouve aucune ligne, et EF Core lance une DbUpdateConcurrencyException.
Il suffit d’attraper cette exception et de dire à l’utilisateur : “Oups, réessayez svp”.
Rate Limiting
Le Rate Limiting (limitation de débit) est une technique essentielle pour protéger une API contre les abus, les attaques par déni de service (DoS) et pour gérer équitablement les ressources du serveur.
Cela consiste à limiter le nombre de requêtes qu’un client (identifié par son IP ou son ID utilisateur) peut effectuer dans un intervalle de temps donné.
Le concept
Imaginez un tourniquet à l’entrée d’un métro. Si tout le monde essaie de passer en même temps, ça bloque. Le Rate Limiting, c’est comme imposer un délai d’une seconde entre chaque passage.
En ASP.NET Core, depuis la version 7, un middleware de Rate Limiting est intégré nativement.
Configuration
1. Enregistrement du service
Dans Program.cs, on configure les règles (policies) de limitation.
Il existe plusieurs algorithmes. Le plus courant est la Fixed Window (Fenêtre fixe). Par exemple : “Max 10 requêtes par minute”.
using Microsoft.AspNetCore.RateLimiting;
using System.Threading.RateLimiting;
// ...
builder.Services.AddRateLimiter(options =>
{
// Rejet avec le code 429 Too Many Requests
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Définition d'une politique nommée "fixed"
options.AddFixedWindowLimiter("fixed", limiterOptions =>
{
limiterOptions.PermitLimit = 10; // Max 10 requêtes
limiterOptions.Window = TimeSpan.FromSeconds(10); // Toutes les 10 secondes
limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
limiterOptions.QueueLimit = 0; // Pas de file d'attente
});
});
2. Activation du middleware
Comme toujours, il faut l’ajouter au pipeline dans Program.cs.
app.UseRateLimiter();
Utilisation
Une fois configuré, vous pouvez appliquer le Rate Limiting sur vos contrôleurs ou vos routes avec l’attribut [EnableRateLimiting].
[ApiController]
[Route("api/[controller]")]
[EnableRateLimiting("fixed")] // Applique la politique "fixed" à tout le contrôleur
public class GameController : ControllerBase
{
// ...
}
Vous pouvez aussi désactiver le rate limiting sur une action spécifique :
[DisableRateLimiting]
[HttpGet("status")]
public IActionResult GetStatus()
{
return Ok("Online");
}
Rate Limiting par Utilisateur
L’exemple précédent limite tout le monde globalement ou par IP (selon la configuration par défaut). Pour limiter par utilisateur connecté, il faut utiliser le partitionnement.
options.AddPolicy("user-limit", context =>
{
// On récupère le nom de l'utilisateur (ou son IP s'il n'est pas connecté)
var username = context.User.Identity?.Name ?? context.Connection.RemoteIpAddress?.ToString();
return RateLimitPartition.GetFixedWindowLimiter(username, _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromSeconds(10)
});
});
Cette configuration crée un compteur séparé pour chaque utilisateur. Alice peut faire 5 requêtes, et Bob peut aussi en faire 5, sans qu’ils se bloquent mutuellement.
Background Services (Tâches de fond)
Un serveur web ne sert pas uniquement à répondre à des requêtes HTTP. Parfois, il doit effectuer des tâches en arrière-plan (background tasks) de manière périodique ou continue.
Exemples :
- Envoyer des emails en attente toutes les 5 minutes.
- Nettoyer des fichiers temporaires toutes les heures.
- Mettre à jour un classement (Leaderboard) en temps réel.
- Écouter une file d’attente de messages (RabbitMQ, Azure Service Bus).
ASP.NET Core fournit une abstraction simple pour cela : les Hosted Services.
L’interface IHostedService
Tout service d’arrière-plan doit implémenter IHostedService.
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Cependant, il est plus simple d’hériter de la classe abstraite BackgroundService, qui gère déjà la mécanique de démarrage et d’arrêt.
Créer un Worker
Voici un exemple d’un service qui écrit “Hello” dans la console toutes les 5 secondes.
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public class MyWorker : BackgroundService
{
private readonly ILogger<MyWorker> _logger;
public MyWorker(ILogger<MyWorker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("MyWorker démarré.");
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker en cours d'exécution : {time}", DateTimeOffset.Now);
// Attendre 5 secondes
await Task.Delay(5000, stoppingToken);
}
_logger.LogInformation("MyWorker arrêté.");
}
}
Points clés :
- ExecuteAsync : C’est la méthode principale. Elle doit tourner tant que l’application est en vie.
- while (!stoppingToken.IsCancellationRequested) : Cette boucle permet au service de continuer à tourner indéfiniment.
- stoppingToken : Ce token est annulé quand l’application s’arrête (ex: Ctrl+C). Il est important de le passer à
Task.Delaypour que le service s’arrête proprement et immédiatement.
Enregistrement
Pour que le service démarre automatiquement avec l’application, il faut l’enregistrer dans Program.cs avec AddHostedService.
builder.Services.AddHostedService<MyWorker>();
Accéder aux Services (Scope)
Attention ! Les BackgroundService sont des Singletons. Vous ne pouvez pas injecter directement un service Scoped (comme votre DbContext ou vos Services métiers) dans le constructeur.
Si vous avez besoin d’accéder à la base de données, vous devez créer un scope manuellement.
public class LeaderboardUpdater : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public LeaderboardUpdater(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Création d'un scope manuel
using (var scope = _serviceProvider.CreateScope())
{
// On récupère le service DEPUIS le scope
var userService = scope.ServiceProvider.GetRequiredService<UserService>();
// On peut maintenant utiliser le service
await userService.UpdateLeaderboardAsync();
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
SignalR (Temps réel)
SignalR est une bibliothèque pour ASP.NET Core qui permet d’ajouter des fonctionnalités web en temps réel aux applications.
Pourquoi SignalR ?
Le protocole HTTP classique fonctionne sur le principe Requête -> Réponse. Le client demande, le serveur répond. Si le serveur a une nouvelle information (ex: nouveau message de chat), il ne peut pas l’envoyer directement au client. Le client doit refaire une demande (Polling).
SignalR permet au serveur d’envoyer du contenu aux clients connectés instantanément (Push).
Il utilise sous le capot la meilleure technique de transport disponible :
- WebSockets (Le plus performant, connexion bidirectionnelle persistante).
- Server-Sent Events (SSE).
- Long Polling (Solution de repli).
Le Hub
Le cœur de SignalR est le Hub. C’est une classe qui sert de pipeline de haut niveau pour gérer les communications client-serveur.
Création du Hub
using Microsoft.AspNetCore.SignalR;
public class ChatHub : Hub
{
// Méthode appelée par le client
public async Task SendMessage(string user, string message)
{
// Envoie le message à TOUS les clients connectés
// "ReceiveMessage" est le nom de la méthode qui sera appelée côté JavaScript
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Ciblage des Clients
L’objet Clients permet de choisir qui reçoit le message :
Clients.All: Tout le monde.Clients.Caller: Uniquement celui qui a envoyé la requête (réponse directe).Clients.Others: Tout le monde sauf l’envoyeur.Clients.User(userId): Un utilisateur spécifique (nécessite l’authentification).Clients.Group("MyGroup"): Un groupe d’utilisateurs.
Configuration
Dans Program.cs, il faut ajouter le service et mapper la route du Hub.
var builder = WebApplication.CreateBuilder(args);
// 1. Ajouter les services SignalR
builder.Services.AddSignalR();
var app = builder.Build();
// ... autres middlewares ...
app.MapControllers();
// 2. Mapper le Hub sur une URL spécifique
app.MapHub<ChatHub>("/chatHub");
app.Run();
Envoyer depuis un Contrôleur (IHubContext)
Le Hub gère les connexions, mais parfois on veut envoyer un message suite à une action externe (ex: fin d’un traitement background, appel API REST).
On ne peut pas instancier le Hub nous-mêmes (new ChatHub() ne marchera pas). On doit injecter IHubContext<T>.
[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{
// On injecte le contexte du Hub
private readonly IHubContext<ChatHub> _hubContext;
public NotificationController(IHubContext<ChatHub> hubContext)
{
_hubContext = hubContext;
}
[HttpPost]
public async Task<IActionResult> NotifyUsers(string text)
{
// Envoi à tous les clients depuis le contrôleur
await _hubContext.Clients.All.SendAsync("ReceiveNotification", text);
return Ok();
}
}
Côté Client
Pour se connecter depuis une page web ou une autre application, on utilise une bibliothèque client.
En JavaScript : @microsoft/signalr.
// Création de la connexion
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub") // L'URL définie dans Program.cs
.build();
// Écoute des messages venant du serveur
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user} dit : ${message}`);
// Mettre à jour l'UI ici
});
// Démarrage de la connexion
connection.start()
.then(() => console.log("Connecté à SignalR"))
.catch(err => console.error(err));
// Envoi d'un message vers le serveur
function sendMessage() {
const user = "Alice";
const message = "Salut tout le monde !";
// Appelle la méthode "SendMessage" du Hub C#
connection.invoke("SendMessage", user, message)
.catch(err => console.error(err));
}
Code Coverage (Couverture de Code)
La couverture de code (Code Coverage) est une métrique utilisée pour mesurer la quantité de code source exécutée lors du lancement des tests unitaires.
Elle permet de répondre à la question : “Quelle partie de mon application est réellement testée ?”
Pourquoi est-ce important ?
- Identifier les zones non testées : Si vous voyez que votre
UserServiceest couvert à 10% seulement, c’est qu’il manque des tests critiques. - Confiance : Une couverture élevée (souvent 80%+) donne une certaine assurance que les régressions seront détectées.
- Maintenance : Cela aide à repérer le “code mort” (jamais exécuté).
Outils .NET
Dans l’écosystème .NET, l’outil standard est Coverlet.
Il est généralement installé par défaut avec les modèles de projet xUnit (coverlet.collector).
Générer un rapport
1. Lancer l’analyse
Pour lancer les tests en collectant les données de couverture, utilisez l’option --collect :
dotnet test --collect:"XPlat Code Coverage"
Cela va générer un fichier coverage.cobertura.xml dans un sous-dossier de TestResults (avec un GUID aléatoire).
2. Visualiser le rapport
Le fichier XML n’est pas très lisible. Pour avoir un beau rapport HTML, nous utilisons l’outil ReportGenerator.
Installation de l’outil (une seule fois) :
dotnet tool install -g dotnet-reportgenerator-globaltool
Génération du rapport :
reportgenerator -reports:TestResults/**/coverage.cobertura.xml -targetdir:coveragereport -reporttypes:Html
Vous pouvez ensuite ouvrir coveragereport/index.html dans votre navigateur pour naviguer dans votre code et voir ligne par ligne ce qui est couvert (en vert) ou non (en rouge).
Projet
Le but du projet sera de créer une application web. Cette application web devra permettre a un utilisateur de recherher un film et de le marquer comme vu. Il pourra également lister l’ensembler des films qu’il a déjà vu.
Pour réaliser cet applicatif, vous devrez créer :
- Un site web en C# Blazor
- un service web en ASP.Net Core
Semaine 1
Initialisation
Créer une solution dotnet (sln) avec le nom IncrementalGame (vous pouvez choisir un autre nom).
dotnet new sln -n IncrementalGame
Créer un projet (vous pouvez choisir un autre nom que GameServerApi)
dotnet new webapi --use-controllers --use-program-main --no-https -n GameServerApi
Ajouter le projet au sln
dotnet sln add GameServerApi
Si cela a fonctionné vous devriez vous retrouver avec les fichiers suivants dans le dossier GameServerApi:
appsettings.Development.json
appsettings.json
bin/
Controllers/
| WeatherForecastController.cs
obj/
Program.cs
GameServerApi.csproj
GameServerApi.http
Properties/
WeatherForecast.cs
Dotnet CLI
Voici quelques commandes qui vous seront très utiles
Compiler le projet :
dotnet build
Executer le projet :
dotnet run
Executer le projet et recharger lors des modifications :
dotnet watch
Structure
Une fois le projet créé je vous invite a créer un dossier Models dans lequel seront stocké les classes de notre projet.
Vous pouvez dès a présent y déplacer WeatherForecast.cs
Vous pouvez supprimer le controller WeatherForecast ainsi que sa classe dès que vous le souhaitez.
Vous pouvez les garder quelques temps en exemple si vous le désirez.
Création d’un Controller
Vous allez désormais pouvoir créer votre propre Controller.
Pour cela c’est assez simple il existe une commande dotnet.
Il s’agit de dotnet new apicontroller
Elle prend plusieurs paramètres dont:
-n: Le nom du controller-p:n: Le namespace du controller-ac: Si vous voulez ajouter des actions CRUD (Create, Read, Update, Delete)-o: Le dossier dans lequel vous voulez créer le controller
Pour créer un controller User dans le dossier Controllers avec des actions CRUD:
dotnet new apicontroller -n UserController -p:n GameServerApi.Controllers -ac true -o Controllers
Si vous connaissez la structure du controller, vous pouvez très bien créer le fichier à la main. Il est aussi possible de copier un controller existant et de le modifier.
Utilisateur
Voud devrez créer une classe Utilisateur dans le dossier Models (Models/User.cs).
Un utilisateur sera caractérisé par :
- Son Id
- Son pseudo
- Son mot de passe
- Son rôle (User, Admin)
Pour le rôle, utilisez une énumération.
User Controller
Votre premier controller aura pour rôle de gérer les utilisateurs.
Il devra fournir les endpoints suivants:
GET /api/User/{id} // Renvoi l'utilisateur correspondant à l'id
POST /api/User/Register // Renvoi l'utilisateur dont on demande la création
POST /api/User/Login // Renvoi l'utilisateur
GET /api/User/All // Renvoi une liste d'utilisateurs
Pour les endpoints Register et Login, ils prendront en paramètre une classe UserInfo qui contiendra le pseudo et le mot de passe.
Le controlleur retournera une classe UserPublic quand il devra retourner un utilisateur, cette classe est similaire a User mais sans le mot de passe.
Ce controller renverra des données codé en dur dans un premier temps. On appelle cela un stub.
Semaine 2
Lors de cette séance on va ajouter une base de données (BDD) à notre service pour pouvoir stocker nos utilisateurs.
Une fois la BDD connectée on modifiera notre controller User pour qu’il puisse l’utiliser.
Base de données
Je vous invite à lire la partie sur les bases de données dans le cours Ici
Vous devrez ajouter une table User dans la BDD avec les champs suivant:
- Id : Id dans la base
- Pseudo : Pseudo de l’utilisateur
- Password : Mot de passe de l’utilisateur
- Role : Le rôle de l’utilisateur (User, Admin)
Modifiez votre classe User en conséquence.
Utiliser la BDD dans un controller
Vous injecterez votre contexte BDD dans le controller User pour pouvoir accéder a la BDD.
Vous modifierez ensuite les méthodes de votre controller pour qu’elles utilisent la BDD.
A la fin de cette séance votre controller devra être capable de:
- Récupérer un utilisateur par son id
- Récupérer un utilisateur par son pseudo et son mot de passe
- Ajouter un utilisateur
- Modifier un utilisateur (Pseudo, Role, Password)
- Supprimer un utilisateur
Voici la liste des endpoints que le controller devra fournir:
GET /api/User/{id} // Renvoi l'utilisateur correspondant à l'id
POST /api/User/Login // Renvoi l'utilisateur si le pseudo et le mot de passe sont correct
POST /api/User/Register // Ajoute un utilisateur en base de donnée et le renvoi
PUT /api/User/{id} // Modifie un utilisateur
DELETE /api/User/{id} // Supprime un utilisateur
GET /api/User/All // Renvoi tout les utilisateurs
GET /api/User/AllAdmin // Renvoi tout les utilisateurs Admin
GET /api/User/Search/{Name} // Renvoi tous les utilisateurs dont le pseudo contient `name`
La route PUT /api/user/{id} prendra en paramètre un objet UserUpdate qui sera une classe qui hérite de UserInfo et lui ajoute le champ Role.
Hasher les mots de passe
Il est important de ne pas stocker les mots de passe en clair dans la BDD. Pour cela on va hasher les mots de passe avant de les stocker.
Je vous invite à lire la partie sur le password hasher dans le cours Ici
Pour utiliser le password hasher, vous devrez l’injecter dans votre controller User.
Pour savoir comment faire, consultez le cours sur l’injection de dépendance Ici
Quand un utilisateur s’enregistre, vous devrez hasher son mot de passe avant de le stocker dans la BDD. Puis quand un utilisateur se connecte, vous devrez verifier que le mot de passe donné correspond bien au mot de passe hashé en BDD.
Semaine 3
Objectif
Pour cette séance, votre objectif est de pouvoir utiliser toutes les fonctionalités du front https://csharp.nouvet.fr/front3
Pour que ce frontend fonctionne avec votre backend vous devrez implémenter le même protocole de communication.
La liste des modèles de donnée ainsi que la liste des routes se trouve à la fin de cette page.
Prérequis
Pour que la communication puisse se faire, il y a 2 pré requis:
- le backend doit être en cours d’exécution avec le port 5000
- vous devez autoriser les requêtes CORS pour l’URL du frontend Doc CORS
Pour changer le port de votre backend, vous pouvez le faire dans le fichier Properties/launchSettings.json.
Gitignore
Je vous invite à ajouter ce fichier .gitignore dans votre projet.
https://github.com/microsoft/dotnet/blob/main/.gitignore
Il vous permettra de ne pas commiter les fichiers de build, les fichiers de configuration, les fichiers de log, etc.
Records
Je vous demanderais d’utiliser des records pour les DTO (Data Transfer Object). Doc Record
Un DTO est un objet qui est transmis entre le frontend et le backend ou l’inverse.
Dans votre cas, cela inclut tout les modèle sauf ceux qui sont stocké en base de données comme User et Progression.
Retour d’erreur
Vous devrez retourner des ErrorResponse en cas d’erreur.
public record ErrorResponse(string Message, string Code);
Vous trouverez les code associés dans les détails de chaque endpoint.
**Errors:**
- `404` - `USER_NOT_FOUND` - User not found
Ici on retourne un code 404 et un message d’erreur avec le code USER_NOT_FOUND.
Dans le code ca ressemblerait à ceci:
return NotFound(new ErrorResponse("User not found", "USER_NOT_FOUND"));
Le front s’attend à certains codes d’erreur et vous devrez les retourner avec les codes exacts.
Controller Game
Vous devrez créer un controller GameController qui sera chargé de gérer les interactions avec le jeu.
En plus du controlleur de jeu vous ajouterez une table Progression dans la BDD pour stocker les données du jeu.
Pour les champs de la classe Progression, regardez le modèle de données Progression en bas de page.
Le controleur devra pouvoir:
- Récupérer la progression d’un utilisateur
- Initialiser la progression d’un utilisateur
- Augmenter le score d’un utilisateur
- Récupérer le coût d’un reset
- Reset le score d’un utilisateur et incrementer le multiplicateur
- Récupérer le score le plus haut en base de données
Ordre d’implémentation
Dans l’ordre, voici les endpoints que vous devrez implémenter:
- Register
- Login
- Get User Progression
- Initialize Progression
- Click
- Get Reset Cost
- Reset Progression
- Get User by ID
- Get Best Score
- Get All Users
- Delete User
- Update User
- Get All Admin Users
- Search Users
Game Design
Le jeu est un jeu de clicker.
L’utilisateur clique sur un bouton pour augmenter son score.
Le score est augmenté de 1 a chaque clic.
Quand l’utilisateur à assez de point, il peut reset son score et le multiplicateur augmente de 1.
Voici la formule pour calculer le coût d’un reset:
private int CalculateResetCost(int multiplier)
{
// Exponential cost: 100 * (1.5^(multiplier-1))
double baseCost = 100.0;
double growthFactor = 1.5;
double cost = baseCost * Math.Pow(growthFactor, multiplier - 1);
return (int)Math.Floor(cost);
}
Contrainte et rôle des utilisateurs
Quand un utilisateur s’inscrit, il aura par défaut le rôle User.
Sauf si aucun utilisateur Admin n’existe en base de données, alors le nouvel utilisateur sera automatiquement Admin.
Le pseudo doit être unique en base de données.
Validation des données
Vous utiliserez les annotations de data annotation pour valider les données du UserPass et du UserUpdate.
Vous implémenterez les règles suivantes:
- Le mot de passe doit être d’au moins 4 caractères et comporter uniquement des lettres, des chiffres et les caractères spéciaux
&^!@#. - Le pseudo doit être d’au moins 3 caractères alphanumériques.
- Les longueurs maximales sont de 20 caractères pour le pseudo et le mot de passe.
- Le champ sont marqués comme obligatoires.
Vous trouverez des exemples de validation dans la documentation sur les data annotations.
Game Server API Documentation
Base URL: http://localhost:5000/api
User Endpoints
Get User by ID
GET /api/User/{id}
Returns: UserPublic
Errors:
404-USER_NOT_FOUND- User not found
Login
POST /api/User/Login
Body: UserPass
Returns: UserPublic
Errors:
404-USER_NOT_FOUND- User not found401-INVALID_PASSWORD- Invalid password
Register
POST /api/User/Register
Body: UserPass
Returns: UserPublic
Errors:
400-USERNAME_EXISTS- Username already exists400-REGISTRATION_FAILED- Registration failed
Update User
PUT /api/User/{id}
Body: UserUpdate
Returns: User (full user object)
Errors:
404-USER_NOT_FOUND- User not found
Delete User
DELETE /api/User/{id}
Returns: true
Errors:
404-USER_NOT_FOUND- User not found
Note: Deletes user’s progression automatically (cascade)
Get All Users
GET /api/User/All
Returns: UserPublic[]
Get All Admin Users
GET /api/User/AllAdmin
Returns: UserPublic[]
Search Users
GET /api/User/Search/{name}
Returns: UserPublic[]
Game Endpoints
Click
GET /api/Game/Click/{userId}
Returns:
{
"count": "integer",
"multiplier": "integer"
}
Errors:
400-NO_PROGRESSION- User does not have a progression
Get Progression
GET /api/Game/Progression/{userId}
Returns: Progression
Errors:
400-NO_PROGRESSION- User does not have a progression
Initialize Progression
GET /api/Game/Initialize/{userId}
Returns: Progression
Errors:
400-PROGRESSION_EXISTS- User already has a progression400-INITIALIZATION_FAILED- Failed to initialize progression
Reset Progression
POST /api/Game/Reset/{userId}
Body: {}
Returns: Progression
Errors:
400-NO_PROGRESSION- User does not have a progression400-INSUFFICIENT_CLICKS- Not enough clicks to reset
Note: Resets count to 0, increments multiplier by 1, updates best score if current is higher
Cost Formula: 100 * (1.5^(multiplier-1))
Get Reset Cost
GET /api/Game/ResetCost/{userId}
Returns:
{
"cost": "integer",
}
Errors:
400-NO_PROGRESSION- User does not have a progression
Get Best Score
GET /api/Game/BestScore
Returns:
{
"userId": "integer",
"bestScore": "integer"
}
Errors:
404-NO_PROGRESSIONS- No progressions found
Error Codes Reference
| Code | Status | Description |
|---|---|---|
USER_NOT_FOUND | 404 | User not found |
INVALID_PASSWORD | 401 | Invalid password |
USERNAME_EXISTS | 400 | Username already taken |
REGISTRATION_FAILED | 400 | Registration failed |
NO_PROGRESSION | 400 | User has no progression |
PROGRESSION_EXISTS | 400 | Progression already exists |
INITIALIZATION_FAILED | 400 | Failed to initialize |
INSUFFICIENT_CLICKS | 400 | Not enough clicks to reset |
NO_PROGRESSIONS | 404 | No progressions found |
Data Models
User
{
"id": "integer",
"username": "string",
"password": "string",
"role": "integer" // 0 = Admin, 1 = User
}
UserPass
{
"username": "string",
"password": "string"
}
UserPublic
{
"id": "integer",
"username": "string",
"role": "integer" // 0 = Admin, 1 = User
}
UserUpdate
{
"username": "string",
"password": "string",
"role": "integer"
}
Progression
{
"id": "integer",
"userId": "integer",
"count": "integer",
"multiplier": "integer",
"bestScore": "integer"
}
ErrorResponse
{
"message": "string",
"code": "string"
}
Semaine 4
Le lien du discord est : https://discord.gg/rQqXeVpk62
Cette semaine nous allons ajouter une boutique dans le jeu.
Version du front : https://csharp.nouvet.fr/front4/
Boutique
La boutique listera les objets disponible à l’achat. Le joueur pourra acheter des objets pour augmenter sa valeur de click.
Lors du reset, le joueur perdra tous ses objets achetés.
Objets
Les objets seront stockés dans la table Item de la BDD. Cf Modèle de donné en base de page.
Inventaire
L’inventaire sera stocké dans la table Inventories de la BDD. Cf Modèle de donné en base de page.
Achats
Lorsque l’utilisateur achète un objet, il sera ajouté à son inventaire. Si l’objet est déjà dans l’inventaire, sa quantité sera augmentée. Si l’objet n’est pas dans l’inventaire, il sera ajouté.
La valeur totalClickValue de la progression sera augmentée de la valeur clickValue de l’objet acheté.
Reset
Lors du reset, les objets de l’inventaire seront vidés.
La valeur totalClickValue de la progression sera réinitialisée à 0.
Click
Lorsque l’utilisateur clique, la valeur totalClickValue de la progression sera mulitpliée par la valeur multiplier de la progression pour calculer combien de clic sera ajouté au compteur.
Controller Inventory
Vous devrez créer un controlleur InventoryController qui sera chargé de gérer les interactions avec l’inventaire.
Seed la base de données avec les objets
On va ajouter une route qui permet de remplir la table Item de la BDD avec des données des objets.
Dans un premier temps l’appel à cette route vide la table Item et la table Inventories puis insert des objets en base de données.
Dans un second temps vous utiliser le HttpClient pour récupérer les objets depuis le fichier items.json et les insérer dans la table Item.
Le fichier se trouve sur https://csharp.nouvet.fr/front4/items.json
Vous trouverez des exemples de HttpClient dans la documentation sur les HttpClient.
Game Server API Documentation
Base URL: http://localhost:5000/api
User Endpoints
Seed Inventory
Vide les objets de la db et des inventaires des utilisateurs puis insert des objets en base de données.
GET /api/Inventory/Seed
Body: {}
Returns: boolean
Errors:
400-SEED_FAILED- Failed to seed inventory (optionnel)
Liste des objets disponibles à l’achat
Liste tout les objets disponibles à l’achat.
GET /api/Inventory/Items
Body: {}
Returns: Item[]
Errors:
404-NO_ITEMS- No items found
Liste des objets dans l’inventaire d’un utilisateur
Liste tout les objets dans l’inventaire d’un utilisateur.
GET /api/Inventory/UserInventory/{userId}
Body: {}
Returns: InventoryEntry[]
Errors:
Achat d’un objet
Achete un objet pour un utilisateur.
L’objet sera ajouté à l’inventaire de l’utilisateur.
La valeur totalClickValue de la progression sera augmentée de la valeur clickValue de l’objet acheté.
Puis retourne l’inventaire de l’utilisateur.
POST /api/Inventory/Buy/{userId}/{itemId}
Body: {}
Returns: InventoryEntry[]
Errors:
400-NOT_ENOUGH_MONEY- Not enough money to buy the item400-ITEM_NOT_FOUND- Item not found400-INVENTORY_FULL- Inventory is full400-USER_NOT_FOUND- User not found
Data Models
Item
{
"id": "integer",
"name": "string",
"price": "integer",
"maxQuantity": "integer",
"clickValue": "integer"
}
InventoryEntry
{
"id": "integer",
"userId": "integer",
"itemId": "integer",
"quantity": "integer"
}
Progression
{
"id": "integer",
"userId": "integer",
"count": "integer",
"totalClickValue": "integer",
"multiplier": "integer",
"bestScore": "integer"
}
Semaine 5
Cette semaine nous allons ajouter une couche de sécurité et d’authentification avec des tokens JWT.
Version du front : https://csharp.nouvet.fr/front5/
Authentification JWT
Référez-vous à la documentation sur l’authentification pour comprendre le fonctionnement des JWT et leur implémentation.
Résumé des étapes
- Installer le package
Microsoft.AspNetCore.Authentication.JwtBearer - Créer un service
JwtServicepour générer les tokens - Configurer la validation JWT dans
Program.cs - Protéger les routes avec
[Authorize]
Création du service JWT
Créez un nouveau fichier Services/JwtService.cs qui contiendra une méthode GenerateToken(User user) retournant un token JWT.
Le token doit contenir les claims suivants :
ClaimTypes.NameIdentifier: l’ID de l’utilisateurClaimTypes.Name: le nom d’utilisateurClaimTypes.Role: le rôle de l’utilisateur
Référez-vous à la section Générer un JWT pour l’implémentation.
Configuration dans Program.cs
Ajoutez la configuration de l’authentification JWT. Voir la section Valider un JWT.
N’oubliez pas d’ajouter le service en AddScoped dans le Program.cs.
Modification du UserController
Injection du service JWT
Injectez le JwtService dans le constructeur du contrôleur.
Modification des routes Login et Register
Les routes Login et Register doivent maintenant retourner un token en plus des informations de l’utilisateur :
var token = _jwtService.GenerateToken(user);
return Ok(new { token = token, user = UserPublic.FromUser(user) });
Ces routes doivent rester accessibles sans authentification avec [AllowAnonymous].
Protection des routes
Ajoutez [Authorize] au niveau du contrôleur. Voir la section Vérification de l’authentification.
Pour les routes sensibles réservées aux administrateurs, utilisez [Authorize(Roles = "Admin")] :
PUT /api/User/{id}DELETE /api/User/{id}
Récupérer l’ID utilisateur depuis le token
Au lieu de passer le userId en paramètre de route, récupérez-le depuis le token. Voir la section Récupération de l’utilisateur.
Ajoutez cette méthode helper dans vos contrôleurs :
private int? GetUserId()
{
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier);
if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out int userId))
{
return null;
}
return userId;
}
Modification des routes
GameController
Toutes les routes doivent être protégées et ne plus prendre le userId en paramètre :
| Ancienne route | Nouvelle route |
|---|---|
GET /api/Game/Click/{userId} | GET /api/Game/Click |
GET /api/Game/Progression/{userId} | GET /api/Game/Progression |
GET /api/Game/Initialize/{userId} | GET /api/Game/Initialize |
POST /api/Game/Reset/{userId} | POST /api/Game/Reset |
GET /api/Game/ResetCost/{userId} | GET /api/Game/ResetCost |
Game Server API Documentation - Semaine 5
Authorization: Bearer <token>
Base URL: http://localhost:5000/api
Tables des autorisations par contrôleur
UserController
| Route | Méthode | Autorisation |
|---|---|---|
/api/User/Login | POST | Public |
/api/User/Register | POST | Public |
/api/User/{id} | GET | [Authorize] |
/api/User/{id} | PUT | [Authorize(Roles = "Admin")] |
/api/User/{id} | DELETE | [Authorize(Roles = "Admin")] |
/api/User/All | GET | [Authorize] |
/api/User/AllAdmin | GET | [Authorize(Roles = "Admin")] |
/api/User/Search/{name} | GET | [Authorize] |
GameController
| Route | Méthode | Autorisation |
|---|---|---|
/api/Game/Click | GET | [Authorize] |
/api/Game/Progression | GET | [Authorize] |
/api/Game/Initialize | GET | [Authorize] |
/api/Game/Reset | POST | [Authorize] |
/api/Game/ResetCost | GET | [Authorize] |
/api/Game/BestScore | GET | [Authorize] |
InventoryController
| Route | Méthode | Autorisation |
|---|---|---|
/api/Inventory/Seed | GET | Public |
/api/Inventory/Items | GET | Public |
/api/Inventory/UserInventory | GET | [Authorize] |
/api/Inventory/Buy/{itemId} | POST | [Authorize] |
Routes avec modification du retour
Login
POST /api/User/Login
Body:
{
"username": "string",
"password": "string"
}
Ancien retour: UserPublic
Nouveau retour:
{
"token": "string",
"user": UserPublic
}
Register
POST /api/User/Register
Body:
{
"username": "string",
"password": "string"
}
Ancien retour: UserPublic
Nouveau retour:
{
"token": "string",
"user": UserPublic
}
Semaine 6
Cette semaine nous allons restructurer notre application pour la rendre plus maintenable, plus testable et plus propre.
Objectifs
Nous avons trois objectifs principaux pour cette séance :
- Architecture en Services : Sortir la logique métier des Contrôleurs.
- Gestion globale des erreurs : Supprimer les répétitions de
try/catchgrâce à un Middleware. - Intégrité des données : Utiliser des Transactions pour sécuriser les achats.
Partie 1 : Architecture en Services
Actuellement, vos contrôleurs contiennent probablement toute la logique : accès à la BDD, calculs, règles métier.
Nous allons alléger les contrôleurs pour qu’ils ne s’occupent que de leur rôle : recevoir des requêtes HTTP et renvoyer des réponses HTTP.
1. Extraction des Services
Consignes
- Créez un dossier
Services. - Créez les classes suivantes :
UserServiceGameServiceInventoryService
- N’oubliez pas d’enregistrer ces services dans
Program.csavecbuilder.Services.AddScoped<...>();. - Déplacez la logique de vos contrôleurs vers ces services.
Exemple de responsabilité :
- Le Contrôleur récupère l’ID utilisateur depuis le Token, appelle le Service, et transforme le résultat en 200 OK ou renvoie une erreur.
- Le Service contient le
DbContext, cherche l’utilisateur en BDD, vérifie s’il a assez d’argent, effectue l’achat, sauvegarde en BDD, et retourne le nouvel état (ou lance une exception).
Référez-vous au cours sur L’architecture Services.
Partie 2 : Middleware et Gestion d’Erreurs
Une fois vos services en place, nous allons nettoyer la gestion des erreurs.
2. Création des Exceptions Personnalisées
Commencez par créer un dossier Exceptions.
Vous créerez une classe GameException qui hérite de Exception et qui aura les propriétés Message, Code et StatusCode.
Remplacez les exceptions génériques dans votre code par ces exceptions personnalisées.
3. Le Middleware de Gestion d’Erreurs
Créez un dossier Middlewares et ajoutez une classe ErrorHandlingMiddleware.cs.
Ce middleware devra :
- Contenir un bloc
try/catchqui englobe l’appel au middleware suivant (await _next(context)). - Dans le
catch, appeler une méthode privée qui va gérer l’exception. - Cette méthode devra définir le
StatusCodede la réponse HTTP et écrire le JSON d’erreur dans le corps de la réponse.
Vous devrez mapper vos exceptions personnalisées aux codes HTTP et codes d’erreurs fonctionnels attendus par le frontend.
Par exemple :
GameException-> Status Code + ErrorResponseException(toutes les autres) -> 500 Internal Server Error + CodeINTERNAL_SERVER_ERROR
Référez-vous au cours sur les Middlewares pour la structure de base.
4. Enregistrement dans Program.cs
N’oubliez pas d’enregistrer votre middleware dans le pipeline de requête dans Program.cs.
5. Refactoring des Contrôleurs
C’est l’étape la plus importante. Vous allez devoir supprimer la gestion d’erreur manuelle de vos contrôleurs.
Vos méthodes de contrôleur ne devraient plus retourner de ActionResult en cas d’erreur (NotFound(), BadRequest()), mais laisser l’exception se propager.
Objectif : Le code de vos contrôleurs doit se concentrer uniquement sur le “Happy Path” (le cas où tout fonctionne).
Exemple conceptuel de ce à quoi cela doit ressembler :
[HttpGet("{id}")]
public ActionResult<UserPublic> GetUser(int id)
{
// Le service lance une exception si l'user n'existe pas.
// Le contrôleur ne s'en occupe plus.
var user = _userService.GetById(id);
return Ok(user);
}
Vous devrez donc aussi modifier vos Services pour qu’ils lèvent les exceptions que vous avez créées au lieu de retourner null ou des codes d’erreur.
Partie 3 : Transactions
Dans la fonction d’achat d’un objet (dans InventoryService), vous effectuez deux opérations critiques :
- Débiter l’utilisateur (modification de
Progression). - Ajouter l’objet (modification de
Inventory).
Si le serveur plante ou si une erreur survient entre ces deux étapes, vous risquez de débiter l’utilisateur sans lui donner l’objet, ou l’inverse.
6. Sécuriser les achats
Utilisez une transaction explicite d’Entity Framework pour englober ces deux opérations.
using var transaction = _context.Database.BeginTransaction();
try {
// 1. Débiter l'argent
// 2. Ajouter l'item
// 3. SaveChanges
transaction.Commit();
} catch {
transaction.Rollback();
throw;
}
Référez-vous au cours sur les Transactions.
Tâches à réaliser
- Extraire la logique métier vers des Services (
UserService,GameService,InventoryService). - Créer les exceptions personnalisées.
- Implémenter le
ErrorHandlingMiddleware. - Configurer
Program.cspour utiliser les services et le middleware. - Refactoriser tout le
GameController,UserControlleretInventoryControllerpour utiliser les services et supprimer lestry/catch. - Ajouter une Transaction dans la méthode d’achat de l’
InventoryService. - (Bonus) Ajouter des logs dans le middleware pour les erreurs 500.
Semaine 7
Cette semaine, nous allons nous concentrer sur la qualité et la robustesse de notre application. Nous allons introduire deux concepts fondamentaux pour tout développement professionnel : les Tests Unitaires et le Logging.
Objectifs
- Logging : Mettre en place un système de journalisation pour suivre l’activité de l’application et diagnostiquer les problèmes.
- Tests Unitaires : Créer un projet de test et écrire nos premiers tests pour valider la logique métier de nos services.
Partie 1 : Le Logging
Le logging (journalisation) est essentiel pour comprendre ce qui se passe dans votre application une fois qu’elle est déployée. C’est votre “boîte noire”.
1. Le concept
En .NET, le logging est intégré nativement via l’interface ILogger<T>.
Il existe plusieurs niveaux de logs :
- Trace / Debug : Pour le développement, très verbeux.
- Information : Flux normal de l’application (ex: “Utilisateur connecté”).
- Warning : Quelque chose d’inattendu mais pas bloquant (ex: “Tentative de connexion échouée”).
- Error : Erreur bloquante ou exception gérée (ex: “Erreur lors du paiement”).
- Critical : Crash de l’application.
2. Injecter le Logger
Nous allons ajouter des logs dans notre application.
Consignes
Utilisez l’injection de dépendance pour injecter ILogger<MaClasse> dans vos services, middlewares et contrôleurs.
3. Ajouter des Logs
Ajoutez ensuite du logging dans vos méthodes.
- Logger les erreurs en LogError dans votre middleware ErrorHandlingMiddleware.
- Ajoutez du logs dans vos services pour tracer les actions importantes (Achat d’item, création de compte, Click, etc.).
- Ajoutez du logs dans vos contrôleurs pour remonter les erreurs et les actions importantes si applicable.
- Ajoutez un middleware qui log en Debug les requêtes et les réponses faites à votre API.
- Dans le Program.cs, ajoutez un logging pour indiquer que l’application est en cours de démarrage et que l’initialisation est terminée.
Note : Pour le logging dans le Program.cs, vous pouvez utiliser le logger injecté dans la variable
app.Logger. Example:app.Logger.LogInformation("Application is starting up...");
Note : Utilisez les
{}pour passer des paramètres au message de log (Structured Logging). Ne faites pas de concaténation de chaînes ("User " + username). Example:_logger.LogInformation("User {Username} logged in", username);
4. Astuce : Trop de logs ?
Par défaut, Entity Framework affiche les requêtes SQL exécutées, ce qui peut polluer votre console.
Pour réduire le bruit, vous pouvez modifier le fichier appsettings.json pour augmenter le niveau minimum de log requis pour EF Core.
Dans appsettings.json :
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning" // N'affiche que les warnings et erreurs de EF
}
}
Partie 2 : Tests Unitaires
Les tests unitaires servent à vérifier qu’une petite partie de code (une “unité”, souvent une méthode) fonctionne comme prévu, indépendamment du reste du système (base de données, réseau, etc.).
1. Création du projet de test
Nous allons utiliser xUnit, le framework de test standard en .NET.
Consignes
- À la racine de votre solution (dossier parent de
GameServerApi), créez un nouveau projet de test :dotnet new xunit -o GameServerApi.Tests - Ajoutez le projet à la solution :
dotnet sln add GameServerApi.Tests - Ajoutez une référence vers votre projet principal :
cd GameServerApi.Tests dotnet add reference ../GameServerApi/GameServerApi.csproj
2. Les Outils de Mocking
Pour tester nos services sans utiliser une vraie base de données nous avons besoin de simuler (“mocker”) le comportement de nos dépendances.
Nous allons utiliser deux bibliothèques :
- Moq : Pour créer des faux objets (mocking de services).
- Microsoft.EntityFrameworkCore.InMemory : Pour simuler la base de données en mémoire.
Installez ces packages dans le projet GameServerApi.Tests :
dotnet add package Moq
dotnet add package Microsoft.EntityFrameworkCore.InMemory --version "9.*"
Important : Configuration du Context
Si vous avez configuré votre BDDContext pour utiliser SQLite directement dans la méthode OnConfiguring, cela va entrer en conflit avec la base de données en mémoire utilisée pour les tests.
Il faut modifier GameServerApi/Models/BDDContext.cs pour ne configurer SQLite que si aucune autre configuration n’a été fournie :
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// Vérifie si une configuration (comme InMemory pour les tests) est déjà présente
if (!options.IsConfigured)
{
// Connexion à la base sqlite par défaut
options.UseSqlite("Data Source=BDD.db");
}
}
3. Premier Test : UserService
Nous allons tester la méthode RegisterAsync de UserService.
Créez un fichier UserServiceTests.cs dans le projet de test.
Structure d’un test (AAA)
- Arrange : Préparer les données et les mocks.
- Act : Exécuter la méthode à tester.
- Assert : Vérifier le résultat.
Exemple de test complet
Voici comment tester que l’inscription fonctionne. Copiez ce code et analysez-le.
using GameServerApi.Models;
using GameServerApi.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Moq;
using Xunit;
namespace GameServerApi.Tests;
public class UserServiceTests
{
[Fact]
public async Task RegisterAsync_ShouldCreateUser_WhenValidData()
{
// 1. ARRANGE
// Setup InMemory Database
var options = new DbContextOptionsBuilder<BDDContext>()
.UseInMemoryDatabase(databaseName: "TestDb_Register") // Nom unique par test
.Options;
var context = new BDDContext(options);
// Setup Mocks
var passwordHasher = new PasswordHasher<User>();
// Pour JwtService, comme ce n'est pas une interface, le plus simple est d'utiliser le vrai
// en lui fournissant une configuration mockée.
var configMock = new Mock<IConfiguration>();
configMock.Setup(c => c["JWTKey"]).Returns("UneCleSecreteTresLonguePourLesTests123456789");
var jwtService = new JwtService(configMock.Object);
var loggerMock = new Mock<ILogger<UserService>>();
// Création du service à tester
var userService = new UserService(context, passwordHasher, jwtService, loggerMock.Object);
// Données de test
var userPass = new UserPass { Username = "TestUser", Password = "Password123!" };
// 2. ACT
var result = await userService.RegisterAsync(userPass);
// 3. ASSERT
Assert.NotNull(result.Token);
Assert.Equal("TestUser", result.User.Username);
// Vérifier que l'user est bien en BDD
var userInDb = await context.Users.FirstOrDefaultAsync(u => u.Username == "TestUser");
Assert.NotNull(userInDb);
Assert.Equal(UserRole.Admin, userInDb.Role); // Le premier user doit être Admin
}
}
4. Lancer les tests
Exécutez la commande suivante :
dotnet test
Vous devriez voir que le test passe (vert).
5. Tester les cas d’erreur
Un bon testeur vérifie aussi que le code échoue quand il le doit.
Ajoutez un test pour vérifier qu’on ne peut pas créer deux utilisateurs avec le même nom.
Tâches à réaliser
-
Logging :
- Injecter
ILoggerdansUserService,GameServiceetInventoryService. - Ajouter des logs (Info/Warning/Error) aux endroits stratégiques.
- Vérifier que les logs apparaissent dans la console quand vous lancez l’API.
- Injecter
-
Tests Unitaires :
- Configurer le projet
GameServerApi.Tests. - Écrire les tests pour
UserService.RegisterAsync(Succès + Erreur doublon). - Écrire les tests pour
UserService.LoginAsync(Succès + Erreur mauvais mot de passe). - Écrire un test pour
InventoryService.PurchaseItemAsync(Vérifier que l’argent est débité et l’item ajouté).
- Configurer le projet
Livrables
Votre projet doit compiler et la commande dotnet test doit réussir avec au moins 5 tests passants.
Semaine 8
Cette semaine nous allons optimiser notre application avec des fonctionnalités avancées pour la rapprocher d’un projet de production.
Lien du front : http://csharp.nouvet.fr/front8/
Rate Limiting
Pour éviter la triche (autoclickers) et protéger le serveur, nous allons limiter le nombre de requêtes possibles sur l’action de click.
Je vous invite à lire la partie sur le Rate Limiting dans le cours Ici.
Configuration
Vous devrez configurer un FixedWindowLimiter dans votre Program.cs.
La limite sera de 10 clics toutes les 10 secondes.
Application
Cette limite devra s’appliquer uniquement sur la route GET /api/Game/Click.
Si un utilisateur dépasse cette limite, il devra recevoir une erreur 429 Too Many Requests.
Revenu Passif
Nous allons ajouter une fonctionnalité de revenu passif. Même quand le joueur ne clique pas, son score doit augmenter légèrement avec le temps.
Je vous invite à lire la partie sur les Background Services dans le cours Ici.
Service
Vous devrez créer un service PassiveIncomeService qui tourne en tâche de fond (Background Service).
Ce service devra ajouter 1 point au score de tous les utilisateurs toutes les 30 secondes.
Vous afficherez un log dans la console à chaque distribution de points.
Test Coverage
Maintenant que vous avez des tests unitaires (Semaine 7), il est intéressant de savoir quelle proportion de votre code est réellement testée.
Je vous invite à lire la partie sur le Code Coverage dans le cours Ici.
Rapport
Vous devrez générer un rapport de couverture HTML pour votre projet.
Vous utiliserez coverlet (inclus de base) et reportgenerator.
L’objectif n’est pas d’atteindre 100%, mais de visualiser les fichiers non testés.
Game Server API Documentation
Base URL: http://localhost:5000/api
Game Endpoints
Click
GET /api/Game/Click
Returns:
{
"count": "integer",
"multiplier": "integer"
}
Errors:
400-NO_PROGRESSION- User does not have a progression429-TOO_MANY_REQUESTS- Rate limit exceeded
Semaine 9
Cette semaine nous allons rendre notre application temps réel grâce à SignalR. Cela permettra d’ajouter un chat global pour que les utilisateurs puissent communiquer entre eux.
Lien du front : http://csharp.nouvet.fr/front9/
SignalR
SignalR est une bibliothèque ASP.NET Core qui simplifie l’ajout de fonctionnalités web en temps réel aux applications.
Configuration
- Ajoutez le service SignalR dans votre
Program.cs. - Mappez le Hub SignalR sur la route
/hub/chat.
ChatHub
Vous devrez créer une classe ChatHub qui hérite de Hub.
Ce hub devra permettre :
- Aux clients d’envoyer un message (
SendMessage) qui sera relayé à tous les autres clients. - De recevoir le nom de l’utilisateur et le contenu du message.
Messages Système
Le chat ne sert pas uniquement aux utilisateurs, le serveur doit aussi pouvoir y poster des informations.
Reset
Lorsqu’un utilisateur effectue un Reset de sa progression (endpoint existant), le serveur doit envoyer un message automatique dans le chat.
Le message doit être de la forme :
SYSTEM: {UserName} a reset son score de {Score} points !
Pour cela, vous devrez injecter IHubContext<ChatHub> dans votre contrôleur ou service qui gère le reset.
CORS pour SignalR
Pour utiliser SignalR, vous devez autoriser les credentials dans la politique CORS. Reférencez-vous au chapitre SignalR CORS pour plus de détails.
Semaine 10
Lien du front : http://csharp.nouvet.fr/front10/
Nouvelle liste d’objets dans le shop : http://csharp.nouvet.fr/front10/items.json
Rendu
Envoyez par mail le lien du repo git à cyril@algorion.fr / benoit_bernay@hotmail.com
Dans le README, précisez les membres de groupes et leur filière.
Prérequis
Les messages SignalR doivent être envoyés depuis les contrôleurs.
Les services devront être testés unitairement.
1. Compteur de joueurs en ligne
Savoir combien de personnes jouent en même temps renforce l’aspect communautaire.
Objectif
- Afficher en temps réel le nombre de connexions WebSocket actives.
Implémentation
- À chaque connexion/déconnexion, envoyez un événement
UpdateUserCountà tous les clients avec la nouvelle valeur. - Le front affichera cette information dans le panneau de chat ou l’en-tête.
2. Notifications de High Score en Temps Réel
Actuellement, on ne sait pas quel est le plus grand score.
Objectif
- Lorsqu’un joueur clic et que son score est le plus haut enregistré, une notification est envoyée à tout le monde.
Implémentation
- Si le record est battu, envoyez un événement
NewHighScoreavec le nom du joueur et le nouveau score. - Garder en cache le top score pour éviter trop de requêtes DB
- Attention : Ne spammez pas ! Si le joueur clique 10 fois par seconde et reste au-dessus du record, n’envoyez pas 10 notifs. Envoyez la notif seulement quand le record est franchi.
3. Annonces d’Achats Épiques
Quand un joueur atteint un niveau suffisant pour acheter l’objet le plus cher du jeu, cela mérite d’être célébré.
Objectif
- Envoyer un message système dans le chat lorsqu’un joueur achète un item qui vaut plus de 10 000 clicks.
Implémentation
- Vérifier si l’item acheté vaut plus de 10 000 clicks.
- Si c’est le cas, envoyez un message via le Hub :
SYSTEM: {UserName} vient d'acquérir {ItemName} !.
4. Reset du score
On va améliorer l’expérience utilisateur en ajoutant des notifications lorsqu’un joueur effectue un reset de son score. On va remplacer le message système par un événement personnalisé.
Objectif
- Envoyer un événement au front qui déclenchera un message dans le chat lorsqu’un joueur effectue un reset de son score avec le nom du joueur et le nouveau score.
Implémentation
- Lorsqu’un joueur effectue un reset de son score, envoyez un événement
PlayerResetà tout le monde avec le nom du joueur et le nouveau score.
5. Gérer l’overflow du compteur de click
Quand le compteur atteint la valeur maximum, plafonnez le à la valeur max. Le but est d’empêcher les valeurs négatives.
6. Actualisation des scores
Quand vous actualisez les scores dans le background service, envoyer un event ScoreUpdate avec comme paramètre le score du joueur.
Vous enverrez un event pour chaque joueur connecté. Les events seront envoyés uniquement au joueur concerné.
Quand un joueur se connecte, il envoie un event Login avec comme paramètre son id.