Sommaire

Notation

La notation sera établi sur le projet rendu à la fin du cours. La date de rendu est le dimanche 2 février 2025 à 23h59.

Merci d'envoyer le lien de votre git à l'adresse suivante: cyril at algorion.fr. Merci de repréciser les membres de votre groupe ainsi que le nom du professeur avec qui vous étiez (NOUVET Cyril ou BERNAY Benoit) dans le mail.

La notation sera basée sur les critères suivants:

Partie API backend:

  • Utilise l'authentification JWT
  • Utilise l'entity framework pour la base de données sqlite
  • Controller User:
    • Récupérer la liste des utilisateurs (Id, Pseudo, Role)
    • Récupérer un utilisateur par son pseudo et son mot de passe (login)
    • Ajouter un utilisateur (register)
    • Modifier un utilisateur (Pseudo, Password, Role)
    • Supprimer un utilisateur
    • Le mot de passe est hashé et n'est pas renvoyé
  • Controller Favorite:
    • Récupérer les favoris d'un utilisateur
    • Ajouter un favori
    • Supprimer un favori
  • Controller Movie:
    • Récupérer les films
    • Supprimer un film
  • Controller OMDB:
    • Rechercher un film par son titre
    • Importer des films depuis l'API OMDB
  • Utilisation de la configuration pour les secrets (Clé d'API OMDB, Secret JWT)
  • Utilisation de l'injection de dépendance
  • Utilisation du méchanisme d'authentification pour protéger les routes
  • Configuration du JWT
  • 2 Services (JWT et OMDB)
  • Gestion des erreurs (try catch, throw)
  • Réponse de code HTTP approprié 200, 404, 500, ...
  • Utilisation de async await pour les appels API, et l'accès BDD

Partie Blazor frontend:

  • Formulaire de login
  • Formulaire d'inscription
  • Page de liste des films:
    • Les films sont affichés sous forme de carte dans un composant
    • On peux ajouter/retirer un film aux favoris
  • Liste des utilisateurs avec leur rôles
  • Page qui affiche les films favoris d'un utilisateur
  • Page admin pour importer des films
  • Possibiliter de se déconnecter
  • Présence de 4 services pour communiquer avec l'API:
    • AuthService
    • UserService
    • FavoriteService
    • MovieService
  • Lors de l'authentification, le token JWT est stocké dans le local storage
  • Le token JWT est envoyé dans le header de chaque requête API qui le nécessite
  • Les pages sont protégées en fonction des rôles
  • Les services sont injectés et appelés dans les composants
  • Utilisation de async await pour les appels API

Si vous avez rajoutez des fonctionnalités, vous pouvez les décrire dans le README.md de votre projet. Elles seront prises en compte dans la notation.

Le projet doit être rendu sur git dans un repository public ou accessible facilement. Vous pouvez nous les envoyez en amont pour avoir un retour sur votre code.

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#.

Discord

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

  1. 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.
  2. 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.
  3. 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 8.0. Pour l'installer, il suffit de suivre les instructions sur le site officiel de .NET.

.NET SDK Download

Veuillez prendre la version SDK 8.0.403.

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

.NET SDK Install in VS CODE

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

Documentation officiel

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

Documentation officiel

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

Documentation officiel

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

Documentation officiel

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

Documentation officiel

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).

Les structs doivent être utilisées avec précaution, car elles peuvent entraîner des problèmes de performance et de comportement inattendu si elles sont mal utilisées. En général on utilise une struct si l'on stocke moins de 16 octets de données.

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, Dictionary<TKey, TValue>, HashSet, 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.

Les classes

C# étant un langage objet, il permet de créer des classes.

Classe

Documentation officielle

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;
    }
}

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

Doumentation officielle

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

Documentation officielle

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.

Les records sont en réalité mutables. Il est possible de modifier les propriétés d'un record. Cependant, il est recommandé de les considérer comme immuables et de mettre le setter à `init`.

Déclaration

Un record est déclaré avec le mot clé record.

public record Personne(string Nom, int Age);

ou comme ceci

public record Personne
{
    public string Nom { get; init; }
    public int Age { get; init; }
}

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
Vous devez utiliser `var` pour déclarer un type anonyme.

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é

Doc Microsoft

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

Documentation officiel

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

Documentation officiel

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

Documentation officiel

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

Documentation Microsoft

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

Documentation officiel

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 classe Program et la fonction Main pour démarrer l'application.
  • --no-https : Désactive le support HTTPS.
  • --name ConfitureApi : Donne le nom ConfitureApi au 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 : Classe Program contenant la fonction Main pour 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 Controllers pour stocker les contrôleurs de l'API
  • le dossier Models (pas crée automatiquement)pour les modèles de données,
  • le dossier Services pour les services utilisés par l'API.
  • le dossier Migrations pour 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

Documentation

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;

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 8.*
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 8.*
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.*
dotnet tool install --global dotnet-ef --version 8.*

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>();
Attention, il est important de ne pas oublier d'ajouter le package `Microsoft.EntityFrameworkCore.Sqlite` pour que le code fonctionne.

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

Attention, il est important d'ajouter le using suivant en haut du Controller pour que le code qui suit fonctionne
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;
    // on indique a notre contexte que l'objet a été modifié
    _context.Entry(confiture).State = EntityState.Modified;

    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 8.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("Id", "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("Id", "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
    });
}

Ajouter notre JWT dans Swagger

Pour tester nos routes sécurisées, nous allons ajouter notre JWT dans Swagger.

Le code suivant permet d'ajouter un bouton Authorize tout en haut du Swagger.

Program.cs

builder.Services.AddSwaggerGen(option => {
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
    {
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 1safsfsdfdfd\"",
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
                {
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Bearer"
                    }
                },
                new string[] {}
        }
    }); 
});

Pour éviter de devoir ajouter le JWT à chaque requête, nous allons demander a Swagger de le retenir. Pour cela, nous allons ajouter une option persistAuthorization à Swagger en modifiant l'appel à UseSwaggerUI.

Program.cs

app.UseSwaggerUI(c =>
{
    c.ConfigObject.AdditionalItems.Add("persistAuthorization","true");
});

Quand vous rentrer votre JWT dans Swagger, n'oubliez pas de mettre `Bearer ` devant ex `Bearer MonJwtSuperLong`

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> :

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");
}

Blazor

Blazor est un framework web open source développé par Microsoft.

Il permet de créer des applications web interactives en C# sans utiliser JavaScript.

Grâce à Blazor, vous pouvez créer des applications web en utilisant le même langage que pour votre API.

L'utilisation des langages HTML et CSS3 reste nécessaire pour la mise en page et le style.

En razor on utilise des fichier .razor qui sont des fichiers HTML avec des balises C#.

Un composant est une unité de code réutilisable qui peut être utilisée dans d'autres composants.

Les fichiers .razor sont des composants, ils peuvent être imbriqués les uns dans les autres.

Création d'un projet Blazor

Pour créer un projet Blazor, vous pouvez utiliser la commande suivante:

dotnet new blazor --no-https --use-program-main -n NomDuFront

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 NomDuFront permet de donner un nom à notre projet, vous pouvez le changer si vous le souhaitez.

Structure

Doc officielle

Une fois le projet créé, vous devriez avoir les dossiers/fichiers suivants:

front/
| Components/ // C'est ici que les composants de l'application seront stockés
|-- Pages/ // Les pages sont des composants qui définissent le contenu de la page
|-- Layout // Les layouts sont des composants qui définissent la structure de la page
|-- App.razor // C'est la première page chargée par l'application
|-- Routes.razor // C'est le routeur de l'application
| wwwroot/ // C'est ici que les fichiers statiques (images, css, js) seront stockés
| Program.cs // C'est le point d'entrée de l'application

Le fichier Program.cs est le point d'entrée de l'application, il contient la méthode Main qui démarre l'application. Il est très similaire a celui de notre API.

Routeur

Le fichier Routes.razor est le routeur de l'application, il permet de définir les routes de l'application.

Vous n'aurez pas besoin de le modifier pour le moment.

En effet, Blazor utilise un où les routes sont définies dans les fichiers dans le dossier Pages.

Pages

Les pages sont des composants qui définissent le contenu de la page web.

Le projet de base contient des exemples de pages, prenez en connaisance pour comprendre comment fonctionne Blazor. Vous pouvez les supprimer si vous le souhaitez.

Page simple

Une page très simple ressemble à ceci:

@page "/mapage"

<PageTitle>Titre de la page</PageTitle>

<h1>Hello, world!</h1>

<p>C'est du HTML</p>

Le @page "/mapage" définit l'URL de la page. Ici la page sera accessible à l'URL http://monsite/mapage.

La balise <PageTitle> permet de définir le titre de la page, c'est un composant fourni par Blazor.

Le reste du code est du HTML classique.

Une page peut contenir des composants imbriqués, des balises C# et des balises HTML.

Page avec paramètre

Votre page peux être appelé avec des paramètre.

Prenons pour exemple une page /random qui nous génère un nombre aléatoire

@page "/random"

<p>@ValeurRandom</p>

@code {
    private static int min = 0;
    private static int max = 10;
    private int ValeurRandom = new Random().Next(min, max);
}

Si l'on veux controller notre valeur min et valeur max on va les ajouter a notre url On aurait donc un lien semblable a cela pour générer un chiffre entre 1 et 10 : http://localhost/random/1/10

@page "/random"
@page "/random/{Min:int}/{Max:int}"

<p>@ValeurRandom</p>

@code {
    [Parameter] public int? Min {get; set;}
    [Parameter] public int? Max {get; set;}
    private int ValeurRandom = -1;

    protected override void OnInitialized() 
    {
        base.OnInitialized();
        ValeurRandom = new Random().Next(Min ?? 42, Max ?? 1337);
    }
}

Vous notterez la présence de 2 @page. Notre page peut donc être appelé via http://localhost/random ou http://localhost/random/1/10.

Le second @page contient 2 paramètres, ce sont les valeurs entre {}.

On a un paramètre min et un paramètre max, par défaut les paramètre sont en string ici on a ajouté :int pour spécifier que l'on attend des entier.

On récupère par la suite les valeurs dans el bloc de code.

Nos paramètres sont annoté avec [Parameter] et la variable est nommé avec le même nom que dans le @page. Les variable sont nullable ? pour gérer le cas ou l'on arrive depuis l'url http://localhost/random .

Page interactive

Une page un peu plus complexe pourrait ressembler à ceci:

@page "/"
@rendermode InteractiveServer
@using System

<PageTitle>Interactive</PageTitle>

<h1>Such wow</h1>

@if (hidden == false)
{
    <p>J'adore les @fruits</p>
} else {
    <p>Caché !</p>
}

<button class="btn btn-primary" @onclick="DoSomething">Do something</button>
<button class="btn btn-primary" @onclick="Hide">Hide/Show</button>

@code {
    private string fruits = "pommes";
    private bool hidden = false;

    private void DoSomething()
    {
        fruits = "cerises";
    }

    private void Hide()
    {
        hidden = !hidden;
    }
}

Ici, nous avons un bouton qui change le texte affiché lorsque l'on clique dessus.

Lors du clic sur le bouton, la méthode DoSomething est appelée et change la valeur de la variable fruits.

Le rendermode InteractiveServer permet d'indiquer que la page est interactive, lorsque l'on appuie sur le bouton, le code est exécuté côté serveur et la page est mise à jour.

Le code C# est défini dans la balise @code et peux être utilisé dans le reste de la page.

Pour déclencher l'appel de la méthode DoSomething lors du clic sur le bouton, on utilise l'attribut @onclick qui prend en paramètre la fonction à appeler.

Vous noterez également la présence d'une condition if qui permet d'afficher ou non un élément en fonction de la valeur de hidden.

Si l'on a besoin d'inclure un namespace, on peut l'ajouter en utilisant la directive @using en haut du fichier.

Composant

Un composant est une partie de l'interface utilisateur qui peut être réutilisée dans plusieurs endroits de l'application.

Par exemple, un formulaire de connexion peut être utilisé sur plusieurs pages.

Création d'un composant

On peut donc créer un composant FormulaireConnexion qui contiendra un formulaire de connexion.

Il ressemblera à ceci:

<form>
    <div>
        <label for="login">Login</label>
        <input type="text" id="login">
    </div>
    <div>
        <label for="password">Mot de passe</label>
        <input type="password" id="password">
    </div>
    <button type="submit">Se connecter</button>
</form>

On peux désormais utiliser ce composant dans une page:

@page "/test"

<PageTitle>Login Page</PageTitle>

<h1>Connectez vous</h1>

<FormulaireConnexion></FormulaireConnexion>

Composant avec paramètres

Il est possible de passer des paramètres à un composant.

Par exemple pour un composant 'Bouton' qui prendra un texte paramètre:

<button>@Text</button>

@code {
    [Parameter]
    public string Text { get; set; }
}

On pourra l'utiliser ainsi:

<Bouton Text="Cliquez ici"></Bouton>

Composant avec événements

Il est possible de déclencher des événements depuis un composant.

Par exemple pour un composant 'Bouton' qui déclenche un événement OnClick:

<button @onclick="OnClick">@Text</button>

@code {
    [Parameter]
    public string Text { get; set; }

    [Parameter]
    public EventCallback OnClick { get; set; }
}

On pourra l'utiliser ainsi:

<Bouton Text="@Text" OnClick="DoSomething"></Bouton>

@code {
    string Text { get; set; } = "Cliquez ici";

    private void DoSomething()
    {
        Console.WriteLine("Clic !");
        Text = "Clic !";
    }
}

EventCallback

Dans l'exemple ci dessus on passe un EventCallback nommé OnClick.

Si vous avez compris l'exemple quand on cliquera sur le bouton cela appelera la fonction que l'on a passé a notre composant.

Si l'on veux appeler cette fonction en dehors d'un bouton, on peux le faire comme ceci

@code {
    [Parameter]
    public EventCallback OnClick { get; set; }

    private void Woot()
    {
        OnClick.InvokeAsync();
    }
}

Si l'on veux renvoyer un paramètre dans notre fonction on ajoute notre type de paramètre comme ceci EventCallback<TypeDuParam>.

@code {
    [Parameter]
    public EventCallback<Pingouin> OnClickWithParam { get; set; }

    private void WootWoot()
    {
        var Pin = new Pingouin() {
            Vivant = false
        };
        OnClickWithParam.InvokeAsync(Pin);
    }

    // A mettre ailleurs
    public class Pingouin {
        public bool Vivant { get; set; }
    }
}

De cette manière on pourra récupérer notre Pingouin dans la fonction appelé


<Banquise OnClickWithParam="Oui" >

@code {
    private void Oui(Pingouin p) {
        Console.WriteLine(p.Vivant);
    }
}

Injection de dépendance

Pour utiliser un service depuis vos pages il faut l'injecter.

Pour cela on ajoute @inject en haut de notre page.

@page "/test"
@inject ClasseDuService service

<h1>Yes</h1>

@code {

    private void test() {
        service.FonctionDuService();
    }

}

Ensuite il faut ajouter notre service en dépendance dans notre Program.cs

builder.Services.AddScoped<ClasseDuService>();

Intéraction

Votre projet front est composé d'un serveur qui renvoi du code HTML a votre navigateur quand vous chargez une page.

Par défaut les composants sont affichés de manière statique. Cela veux dire que quand vous intéragissez avec votre page vous ne la modifiez que de votre coté. Le serveur lui ne reçoit pas vos intéraction.

Voici a quoi ca ressemble Rendermode

Votre navigateur demande une page au serveur Si le serveur connait cette page il lui renvoi son code HTML Le client modifie le champ texte sur son navigateur Rien ne se passe coté serveur Le client clic sur le bouton sur son navigateur Rien ne se passe coté serveur

Si on active le rendu intéractif on a ce schéma

Rendermode

Si le serveur connait cette page il lui renvoi son code HTML Le client modifie le champ texte sur son navigateur Le serveur reçoit le texte modifié Le client clic sur le bouton sur son navigateur Le serveur reçoit l'évenement du clic

Sans le mode de rendu intéractif on en peux pas appeler de code après le chargement de la page. Un formulaire par exemple a besoin de cette intéractivité pour pouvoir mettre a jour le texte et envoyer le clic au serveur pour que celui-ci déclenche les actions voulu.

Comment on active le mode de rendu intéractif ?

Pour l'activer il y a plusieurs manière:

Par composant

On peux ajouter la ligne suivante en haut d'une page ou d'un composant

@rendermode InteractiveServer

Cela rend votre page ou votre composant intéractif

Pour toute l'application

Dans le fichier App.razor passez un paramètre au composant route comme suit :

<Routes @rendermode="InteractiveServer" />

Cela aura pour effet de rendre toute les pages chargé par votre routeur intéractive.

Désactiver le prérendu

Si vous avez besoin d'utiliser certaines fonctionnalité comme l'accès au local storage, le rendu dynamique peut poser problème.

Dans ce cas vous pouvez désactiver le prérendu en ajoutant le paramètre @prerender a false dans le fichier App.razor

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

Service API

Pour comuniquer avec une API, nous allons créer un service qui va gérer les appels HTTP.

Créez un nouveau fichier ConfitureService.cs dans le dossier Services.

On injecte une instance de HttpClient dans le constructeur pour faire des appels HTTP.

namespace MonFront.Services;

public class ConfitureService
{
    private readonly HttpClient _httpClient;

    public ConfitureService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
}

Requêtes HTTP

Pour récupérer la liste des confitures, on utilise la méthode GetFromJsonAsync de HttpClient.

public async Task<List<Confiture>> GetConfitures()
{
    var confitures = await _httpClient.GetFromJsonAsync<List<Confiture>>("http://localhost:5000/api/confiture/list");
    return confitures;
}

Pour récupérer une confiture en particulier, on utilise la méthode GetFromJsonAsync de HttpClient.

public async Task<Confiture> GetConfiture(int id)
{
    var confiture =  await _httpClient.GetFromJsonAsync<Confiture>($"http://localhost:5000/api/confiture/{id}");
    return confiture;
}

Si on veux ajouter une confiture, on utilise la méthode PostAsJsonAsync de HttpClient.

public async Task<HttpResponseMessage> AddConfiture(Confiture confiture)
{
    var confiture =   await _httpClient.PostAsJsonAsync("http://localhost:5000/api/confiture/create", confiture);
    return confiture;
}

Si on veux envoyer une requête sans attendre de réponse en JSON on peux simplement faire

public async Task Ping()
{
    await _httpClient.GetAsync("http://localhost:5000/api/ping");
}

Vérifier le code de retour

Pour vérifier le code de retour d'une requête, on utilise la propriété StatusCode de HttpResponseMessage.

var response = await _httpClient.PostAsJsonAsync("http://localhost:5000/api/confiture/create", confiture);
if (response.StatusCode == HttpStatusCode.Created)
{
    // Code 201
}
var confiture = await response.Content.ReadFromJsonAsync<Confiture>();
return confiture;

Persistence des données

Comme notre code s'éxécute dans le navigateur de l'utilisateur, nous ne pouvons pas stocker les données de la même manière que dans une application classique.

Les moyens les plus souvent utilisés pour stocker des données dans le navigateur sont les suivants:

  • LocalStorage
  • Cookies

Nous allons voir comment utiliser LocalStorage dans notre application Blazor.

ProtectedLocalStorage

Stocker des données

private readonly ProtectedLocalStorage _sessionStorage;
Pokemon pokemon = new Pokemon { Name = "Pikachu", Type = "Electric" };
await _sessionStorage.SetAsync("MonPokemon", pokemon);

Récupérer des données

private readonly ProtectedLocalStorage _sessionStorage;
Pokemon pokemon = await _sessionStorage.GetAsync<Pokemon>("MonPokemon").Value;

Supprimer des données

private readonly ProtectedLocalStorage _sessionStorage;
await _sessionStorage.DeleteAsync("MonPokemon");

Vider le storage

private readonly ProtectedLocalStorage _sessionStorage;
await _sessionStorage.ClearAsync();

Authentification

AuthStateProvider

Pour gérer l'authentification dans Blazor, nous allons utiliser un AuthStateProvider. C'est une interface qui permet de gérer l'état de l'authentification dans notre application.

Créez un nouveau fichier AuthProvider.cs dans le dossier Services.

namespace MonFront.Services;

public class AuthProvider : AuthenticationStateProvider
{
    public AuthProvider()
    {
    }
}

On va injecter une instance de ProtectedLocalStorage dans le constructeur pour stocker l'état de l'authentification. ProtectedLocalStorage est une classe qui permet de stocker des données de manière sécurisée dans le navigateur de l'utilisateur. Nous allons l'utiliser pour stocker le token JWT de l'utilisateur lorsque celui-ci se connecte. Cele permet de garder l'utilisateur connecté même s'il rafraichit la page ou s'il ferme le navigateur.

private readonly ProtectedLocalStorage _sessionStorage;

public AuthProvider(ProtectedLocalStorage protectedSessionStorage)
{
    _sessionStorage = protectedSessionStorage;
}

Pour l'utiliser voici la page du cours : Cours

GetAuthenticationStateAsync

Pour récupérer l'état de l'authentification AuthenticationStateProvider doit implémenter la méthode GetAuthenticationStateAsync.

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var claim = new ClaimsPrincipal(new ClaimsIdentity()); // Créer un objet ClaimsPrincipal vide == utilisateur non connecté
            return new AuthenticationState(claim); // Retourne l'état de l'authentification
        }

Login

Créeons une méthode Login qui prend en paramètre un User et un token JWT et qui les stockent dans le ProtectedLocalStorage. Puis qui génère un ClaimsPrincipal à partir de l'utilisateur et qui notifie le changement d'état de l'authentification.

public async Task Login(User user, string token)
{
    await _sessionStorage.SetAsync("User", user); // Stocke l'utilisateur
    await _sessionStorage.SetAsync("Token", token); // Stocke le token
    ClaimsPrincipal claim = GenerateClaimsPrincipal(user); // Génère un ClaimsPrincipal
    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claim))); // Notifie le changement d'état de connexion
}

public ClaimsPrincipal GenerateClaimsPrincipal(User user)
{
    var claims = new[]
    {
        new Claim("Id", user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Pseudo),
        new Claim(ClaimTypes.Role, user.Role.ToString())
    };
    ClaimsIdentity identity = new ClaimsIdentity(claims, "custom");
    ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(identity);
    return claimsPrincipal;
}

Logout

Créeons une méthode Logout qui supprime l'utilisateur et le token du ProtectedLocalStorage et qui notifie le changement d'état de l'authentification.

public async Task Logout()
{
    await _sessionStorage.DeleteAsync("User"); // Supprime l'utilisateur
    await _sessionStorage.DeleteAsync("Token"); // Supprime le token
    var claimDisconnected = new ClaimsPrincipal(new ClaimsIdentity()); // Créer un objet ClaimsPrincipal vide == utilisateur non connecté
    NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimDisconnected))); // Notifie le changement d'état de connexion
}

Modifier GetAuthenticationStateAsync

Modifions la méthode GetAuthenticationStateAsync pour qu'elle retourne un ClaimsPrincipal si l'utilisateur est connecté.

public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
    var user = await _sessionStorage.GetAsync<User>("User"); // Récupère l'utilisateur
    if (user.Value != null) // Si on a un utilisateur dans le local storage
    {
        var claim = GenerateClaimsPrincipal(user.Value); // Génère un ClaimsPrincipal
        return new AuthenticationState(claim); // Retourne l'état de l'authentification
    }
    return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); // Retourne un utilisateur non connecté
}

Utilisation

Pour utiliser notre AuthProvider dans notre application, nous allons l'injecter dans le Program.cs.

builder.Services.AddScoped<AuthenticationStateProvider, AuthProvider>();

Si nous voulons utiliser notre AuthProvider dans une page, nous allons l'injecter dans le constructeur de la page.

@page "/Login"
@inject NavigationManager NavigationManager
@inject AuthenticationStateProvider AuthStateProvider

@code {

    // login du user sur le authProvider
    private async Task LoginAction(UserInfo userInfo)
    {
        var userAndJwt = await UserService.Login(userInfo.Pseudo, userInfo.Password); // Appel à notre API
        if (userAndJwt != null) // Si l'API nous retourne un utilisateur
        {
            await ((AuthProvider)AuthStateProvider).Login(userAndJwt.User, userAndJwt.Token); // Connexion
            NavigationManager.NavigateTo("/"); // Redirection vers la page d'accueil
        }
    }

    // Redirection si l'utilisateur est déjà connecté
    protected override async Task OnInitializedAsync()
    {
        var returnUrl = GetQueryParm("ReturnUrl");
        if (returnUrl != "")
        {
            var isLogged = await AuthStateProvider.GetAuthenticationStateAsync();
            if (isLogged.User.Identity is not null && isLogged.User.Identity.IsAuthenticated)
            {
                NavigationManager.NavigateTo(returnUrl);
            }
        }
    }
    
    string GetQueryParm(string parmName)
    {
        var uriBuilder = new UriBuilder(NavigationManager.Uri);
        var q = System.Web.HttpUtility.ParseQueryString(uriBuilder.Query);
        return q[parmName] ?? "";
    }
    // Fin de la redirection
    

}

Activation de l'authentification

Pour activer l'authentification dans notre application, nous allons ajouter les lignes suivantes dans le Program.cs

builder.Services.AddAuthentication(o =>
  {
    o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  })
  .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
  {
    options.Cookie.Name = "auth_cookie";
    options.LoginPath = "/Login"; // Redirection vers la page de connexion
  });

builder.Services.AddAuthenticationCore();

...

app.UseAuthentication();
app.UseAuthorization();

Il faudra également modifier le ficheir Components/Router.razor pour indiquer à notre application de vérifier l'authentification avant d'afficher une page.

On change AuthorizeRouteView à la place de RouteView.

<Router AppAssembly="typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
        <FocusOnNavigate RouteData="routeData" Selector="h1" />
    </Found>
</Router>

Authentification des pages

Pour sécuriser une page, nous allons utiliser l'attribut [Authorize] comme dans l'API.

@page "/Admin"
@attribute [Authorize(Roles = "Admin")]

<h1>Page Admin</h1>

Récupérer l'utilisateur connecté

Pour récupérer l'utilisateur connecté, nous allons utiliser la méthode GetAuthenticationStateAsync de AuthenticationStateProvider.

@inject AuthenticationStateProvider AuthenticationStateProvider

@code {
    private bool _isLogged = false;

    private async Task isUserConnected()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        _isLogged = user.Identity?.IsAuthenticated ?? false;
    }

    private async Task<string> getUsername()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var user = authState.User;
        return user.FindFirst(ClaimTypes.Name).Value;
    }
}

AuthorizeView

Affichage en fonction de l'authentification

Voici comment afficher un bouton de connexion ou de déconnexion en fonction de l'authentification.

<AuthorizeView>
    <Authorized>
        <button @onclick="Logout">Déconnexion</button>
    </Authorized>
    <NotAuthorized>
        <button @onclick="Login">Connexion</button>
    </NotAuthorized>
</AuthorizeView>

Affichage en fonction du rôle

Voici comment afficher un bouton en fonction du rôle de l'utilisateur.

Ici on affiche un bouton seulement si l'utilisateur a le rôle Admin.

<AuthorizeView Roles="Admin">
    <Authorized>
        <button @onclick="AdminAction">Action Admin</button>
    </Authorized>
</AuthorizeView>

Utiliser le context

Quand vous utilisez le composant AuthorizeView vous avez accès a une variable context qui contient des informations sur l'utilisateur connecté.

<AuthorizeView>
    <Authorized>
        <p>Bonjour @context.User.Identity.Name</p>
        <p>Votre rôle est @context.User.FindFirst(ClaimTypes.Role).Value</p>
    </Authorized>
</AuthorizeView>

404

Si vous voulez personaliser la page 404, vous pouvez créer un composant NotFound.razor dans le dossier Pages de votre projet Blazor.

@page "/Error/404"

<h3>Page non trouvée</h3>
<p>Désolé, la page que vous recherchez n'existe pas.</p>

Ensuite, dans le fichier Router.razor, vous pouvez rediriger vers cette page si aucune route n'est trouvée.

<Router>
    <Found>
        ...
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(Layout.MainLayout)">
            <NotFound/>
        </LayoutView>
    </NotFound>
</Router>

Il faut également préciser à notre application de rediriger vers cette page si aucune route n'est trouvée. Pour cela, il faut ajouter une ligne spéciale dans le fichier Program.cs de votre projet Blazor.

app.UseStatusCodePagesWithReExecute("/Error/{0}");

Cette ligne permet de rediriger vers la page d'erreur correspondante en fonction du code d'erreur. Le {0} correspond au code d'erreur. Ainsi, si une route n'est pas trouvée, l'utilisateur sera redirigé vers la page /Error/404 qui est celle que nous avons créee.

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 MonTrackerDeFavoris (vous pouvez choisir un autre nom).

dotnet new sln -n MonTrackerDeFavoris

Créer un projet (vous pouvez choisir un autre nom que TrackerDeFavorisApi)

dotnet new webapi --use-controllers --use-program-main --no-https -n TrackerDeFavorisApi

Ajouter le projet au sln

dotnet sln add TrackerDeFavorisApi

Si cela a fonctionné vous devriez vous retrouver avec les fichiers suivants dans le dossier TrackerDeFavorisApi:

appsettings.Development.json
appsettings.json
bin/
Controllers/
| WeatherForecastController.cs
obj/
Program.cs
projet.csproj
projet.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 TrackerDeFavorisApi.Controllers -ac true -o Controllers
Remplacez `TrackerDeFavorisApi` par le nom de votre projet. Celui-ci est le nom que vous avez donné dans la commande `dotnet new webapi`

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 pseudo
  • Son mot de passe
  • Son rôle (User, Admin)

Pour le rôle, utilisez une énumération.

User Controller

Un endpoint ou une route est une adresse web qui permet d'accéder à une ressource.

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

Pour les endpoints register et login, ils prendront en paramètre une classe UserInfo qui contiendra le pseudo et 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

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.

Vous ne renverrez jamais le mot de passe hashé(ou non) dans les réponses de votre service.

Bonus

Ajoutez une méthode pour récupérer tous les utilisateurs de la BDD Ajoutez une méthode pour récupérer tous les utilisateurs admin de la BDD Ajoutez une méthode pour récupérer tous les utilisateurs dont le nom contient une chaîne de caractères donnée

Voici les endpoints correspondant:

GET /api/User // Renvoi tous les utilisateurs mais sans les mots de passe
GET /api/User/admin // Renvoi tous les utilisateurs admin
GET /api/User/search/{name} // Renvoi tous les utilisateurs dont le pseudo contient `name`

Semaine 3

Lors de cette séance nous allons:

  • Ajouter 2 tables a notre BDD : Film et Favorite
  • Créer 2 Controlleur : Film et Favorite
Je vous invite à insérer des données dans la table `Films` et `Favorites` pour pouvoir tester votre controlleur.

Nouvelle table en BDD

Vous devrez ajouter 2 tables dans la BDD et donc dans la classe qui hérite de DbContext, voici les champs a mettre dans chacune des deux tables

Films

  • Id : Id dans la base
  • Title : Titre du film
  • Poster : URL de l'image du film (string)
  • Imdb : Id IMDB (string)
  • Year : Date de sorti

Favorites

  • Id : Id dans la base
  • UserId : Id de l'utilisateur à qui le favori appartient
  • FilmId : Id du film en favori

Nouveau controlleur

Vous devrez ajouter 2 controlleur a votre projet, un pour les films et un pour les favoris.

Film

Dans le controlleur Film nous auront besoin de 3 routes:

Une route qui renverra la liste des films stocké en BDD.

GET /api/Film/
Paramètre:
- Aucun
Retourne:
200 - [{"Id": 1, "Title":"Titre du film","Year":"2000","Poster":"https://lien_du_poster.jpg"}, ...]

Une route qui ira chercher les film qui correspondent aux mots clés.

GET /api/Film/search
Paramètre: 
- title : mots clés pour rechercher le film 
Retourne:
200 - [{"Id": 1, "Title":"Titre du film","Year":"2000","Poster":"https://lien_du_poster.jpg"}, ...]
404 - Si aucun film n'a été trouvé

Une route info qui renvoi les films dont les ids correspondent au paramètre passé

GET /api/Film/info
Paramètre: 
- ids : un tableau d'id des films ex: [1,2,3]
Retourne:
200 - [{"Id": 1, "Title":"Titre du film","Year":"2000","Poster":"https://lien_du_poster.jpg"}, ...]
404 - Si aucun film n'a été trouvé
Vous aurez besoin d'utiliser [FromQuery] pour récupérer les paramètres de la requête. Comme il s'agit d'un tableau d'entier, il faut spécifier ou le chercher contrairement a un type simple.
        // GET api/<Film>/info/
        [HttpGet("info")]
        public async Task<ActionResult<IEnumerable<Film>>> GetFilm([FromQuery] int[] ids)

Favoris

Dans le controlleur Favorite nous auront besoin de plusieurs routes:

N'ayant pas de manière de savoir quel utilisateur fait la requête vous utiliserez un id d'utilisateur en dur pour le moment.

Une route qui ajoutera un film dans la table Favorite de la BDD.

POST /api/Favorite/add
Paramètre: 
- id : l'id du film a mettre en favori
Retourne:
200 - Vide // Si tout a fonctionné
404 - Vide // Si le film n'existe pas

Une route qui supprimera un favori dans la table Favorite de la BDD

DELETE /api/Favorite/remove
Paramètre: 
- id : l'id du film a enlever des favoris
Retourne:
200 - Vide // Si tout a fonctionné
404 - Vide // Si le favori n'a pu être supprimé (car il n'existait pas par exemple)

Une route qui renverra la liste des favoris de l'utilisateur

GET /api/Favorite/list/{userId}
Paramètre: 
- Aucun
Retourne:
200 - [1,2,3,8,150] // id des films en favori pour l'utilisateur

Bonus

Faites que la recherche de film fonctionne peu importe la casse des mots clés. (ex: "lord of the rings" et "Lord Of The Rings" renvoi le même résultat)

Semaine 4

Cette semaine nous allons ajouter une fonctionnalité pour que nos utilisateurs puissent ajouter des vrais films à leurs favoris.

Pour cela nous allons utiliser une API externe pour récupérer des informations sur des films.

Ces films seront stockés dans la table Films.

Requête HTTP

Je vous invite a lire la partie sur les requêtes HTTP dans le cours Ici Ainsi que la partie sur le JSON Ici

Le JSON c'est bien mais à quoi ca va nous servir ? Le JSON est devenu le format le plus commun pour que deux applications qui ne se connaisse pas puissent s'échanger facilement des données.

Dès que notre service va renvoyer une réponse, elle sera en JSON.

Notre service va lui même aller faire des appels a un autre service disponible a tous sur internet.

Requeter des films et serie

Pour que notre utilisateur puisse ajouter des films à ses favoris, il nous faut une liste de film. La création d'une telle liste étant une tâche longue et peu intéressante, nous utiliserons une API externe.

Le site https://www.omdbapi.com fourni une API pour chercher des films par nom.

Obtenir une clé d'API

Pour faire des requête le site vous demandera une clé d'API pour vous authentifier, vous pouvez en obtenir une gratuitement via ce lien : https://www.omdbapi.com/apikey.aspx Cochez FREE, mettez votre mail puis Submit. Une fois le mail reçu, cliquez sur le lien d'activation et attendez 10 minutes que cela soit effectif.

Voici un lien d'exemple avec ma clé d'API pour rechercher ce qui est objectivement la meilleur trilogie existante https://www.omdbapi.com/?s=lord of the rings&page=1&type=movie&apikey=414e8ea4

Il est important de ne pas stocker de clé d'API dans le code source, vous devrez la stocker dans un fichier de configuration. cf [Configuration](../webapi/configuration.md)

Intégration de l'API dans un service

Vous allez intégrer cet API dans votre projet.

Ceci est un exemple d'utilisation d'une API externe, il est important de ne pas abuser des requêtes pour ne pas surcharger les serveurs.

Pour cela vous allez créer une classe OmdbService dans le dossier Services qui aura une méthode SearchByTitle qui prendra en paramètre un string title et renverra une liste de film.

Vous créer ensuite une autre méthode GetByImdbId qui prendra en paramètre un string imdbId et renverra un film.

Vous utiliserez la classe HttpClient pour faire des requêtes HTTP. Allez voir le cours sur l'injection de dépendance pour savoir comment l'injecter Ici Aller également voir le cours sur les requêtes HTTP pour savoir comment les utiliser Ici Et enfin le cours sur le JSON pour savoir comment traiter les réponses Ici

N'oubliez pas de gérer les erreurs et de renvoyer un code d'erreur si la requête n'a pas pu être effectuée. Documentation

Nouvelles classes

Les réponses OMDB sont en JSON, vous devrez donc créer des classes pour désérialiser ces réponses.

Il faudra créer 4 classes:

  • OmdbSearchResponse
  • OmdbFilm
  • OmdbFilmDetail
  • OmdbFilmRating

OmdbSearchResponse contiendra une liste de OmdbFilm et OmdbFilm contiendra les informations d'un film. OmdbFilmDetail contiendra les informations détaillées d'un film et les ratings au format OmdbFilmRating.

Il est possible de générer ces classes automatiquement via Visual Studio ou un site comme json2csharp
Pour typer les champs qui sont des listes, vous pouvez utiliser List<T> ou IEnumerable<T>, le second étant plus générique.
Conservez bien les même noms de propriétés que dans le JSON pour que la désérialisation fonctionne.

Controlleur OMDB

Vous allez créer un controlleur Omdb.

Celui-ci devra utiliser le service OmdbService pour faire des recherches de films.

Ce controlleur fournira les routes suivantes:

GET /api/Omdb/search/{title} // Renvoi les films correspondant au titre
GET /api/Omdb/import/{imdbId} // Recherche un film par son id IMDB, l'ajoute à la BDD puis le retourne

Injection de dépendance

Pour utiliser OmdbService dans votre controlleur vous devrez l'injecter via l'injection de dépendance.

Pour cela vous pourrez utiliser la méthode AddSingleton cf cours Ici

Bonus

Ajouter les champs Plot et Released dans la table Film et remplissez les lors de l'import.

Ajoutez un champ Rating dans la table Film pour stocker la note "Rotten Tomatoes" du film qui est renvoyer dans la recherche par imdbId. Puis stocker cette valeur lors de l'import.

Semaine 5

Cette semaine nous allons ajouter une interface utilisateur à notre projet. Cette interface permettra à nos utilisateurs de:

  • S'inscrire
  • Se connecter
  • Lister les films
  • Ajouter des films à leurs favoris
  • Afficher la liste de leurs films favoris

Nous allons utiliser le framework Blazor pour cela.

Blazor

Je vous invite à lire le cours sur Blazor pour vous familiariser avec ce framework Ici

Travail

Pour cette semaine, vous devrez créer les pages suivantes:

  • Une page d'inscription
  • Une page de connexion
  • Une page de liste de films

Vous devrez à minima avoir les composants suivants:

  • une card pour afficher un film (titre, année, image) Exemple
  • un formulaire avec un bouton, un champ login et un champ mot de passe que vous pourrez réutiliser pour la connexion et l'inscription

Composants partagés

Vous créerez des composants pour faciliter la réutilisation de votre code.

Je vous invite a créer un dossier Shared dans le dossier Components pour y stocker vos composants.

Components/
| Layout/
| Pages/
| Shared/

Il faudra ajouter un using dans le fichier _Imports.razor pour pouvoir utiliser les composants de ce dossier.

@using nomprojet.Components.Shared

Semaine 6

Cette semaine nous allons recupérer depuis notre API pour les afficher dans notre application Blazor.

Vous allez devoir réaliser 4 Services:

  • UserService qui permettra de se connecter et de s'inscrire
  • FilmService qui permettra de récupérer la liste des films
  • FavoriteService qui permettra de récuperer les films favoris de l'utilisateur, d'en ajouter et d'en supprimer
  • OmdbService qui permettra de chercher des films et d'en importer

Semaine 7

Cette semaine nous allons implementer l'authentifications dans nos applications. Nous allons utiliser JWT pour sécuriser nos API et nos pages web.

Je vous invite à lire les pages suivantes:

Pour ce faire, je vous conseille de réaliser les tâches suivantes:

API

  • Créer un service JwtService qui permettra de générer et de valider les tokens JWT
  • Modifier la route de login pour retourner un JWT en plus du User
  • Intégrer la verification du JWT dans votre API (Program.cs)
  • Ajouter la gestion du JWT dans swagger
  • Rendre vos controllers et routes authentifiées en fonction des roles

Blazor

  • Créer un service AuthProvider qui permettra de gérer l'authentification
  • Modifier les pages Login pour utiliser le service AuthProvider après le login
  • Modifier le menu pour afficher les liens en fonction de l'authentification
  • Modifier vos composants en fonction de l'authentification

Voici un schéma de l'architecture que vous devriez avoir:

Architecture

Image en grand format: Architecture