rorodar
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # Créer une API en **.NET 6** *Cet article reprend dans les grandes lignes le précédent sur le .NET 5 tout en mettant à jour les subtilités de cette nouvelle version .NET 6* ## Introduction ### Le .NET 6 ? Le .NET 6, suite direct du .NET 5 monte rapidement ces derniers temps. Ceci n’est pas un hasard ! En effet, cette nouvelle mouture de Microsoft a toujours le même objectif : remplacer le .NET Framework historique. Contrairement à son aîné, cette technologie est OpenSource et a pour vocation de se rendre plus accessible à tous les systèmes d'exploitation. Un tournant dans la sphère Microsoft, qui a débuté avec le .NET CORE en 2016. Pour ce début d'année 2022, que diriez-vous d’une petite veille technologique sur la dernière version de cet éco-système libre ? Et bien, c'est parti ! ### Au programme Pour faire un tour d'horizon de cette technologie, je vais vous proposer une approche plutôt complète. Au dela de la création pure et simple d'une API, nous allons mettre en place tout ce qui gravite autour de ce type projet WEB. Voici la feuille de route que je vous propose : * Création d'une API en **.NET 6** * Mise en place d'une architecture **simple** mais posant les bases d'un découpage fonctionnel * Manipulation d'une base de données locale * Conception de la BDD par l'approche Entity Framework (Code First) * Sécurisation de l'API par Token JWT et rôles utilisateurs ### Pré-requis - [ ] Installer Visual Studio 2022 (https://visualstudio.microsoft.com/fr/downloads/) Penser au moment de l'installation à cocher ces deux options : ![](https://i.imgur.com/w0iFgkw.png) > Lorsque vous sélectionnez "**Développement .NET Desktop**", n’hésitez pas à regarder si l’option "**SQL Server Express LocalDB**" est bien cochée dans la partie droite avant de lancer l’installation. > Installer l'IDE en anglais (Onglet "Modules linguistiques"), pour s'y retrouver parfaitement dans les captures qui vont suivre ;-) - [ ] [Télécharger SQL Management Studio](https://docs.microsoft.com/fr-fr/sql/ssms/download-sql-server-management-studio-ssms), Il s’agit du Système de Gestion de Bases de Données (SGBD) de Microsoft. --- ## Création du projet API Dans Visual Studio 2022, nous allons faire ce grand classique lors du lancement de l'IDE : ![](https://i.imgur.com/MbbQEqr.png) Ensuite, nous allons sélectionner « ASP.NET Core Web API » : ![](https://i.imgur.com/MR1nw28.png) Nommons maintenant le projet WEB, la solution et l’emplacement de tout cela : ![](https://i.imgur.com/LxXr7Dz.png) C’est maintenant que tout commence ! ![](https://i.imgur.com/Ec0l2eE.png) > Faisons attentions à bien choisir : > * la bonne version : **.NET 6.0 (long-term support)** > * Et laissons les autres options par défaut 🎉Félicitations 🎉 Votre API est créée : ![](https://i.imgur.com/tt8mBZ0.png) > Le Template vous génère un contrôleur « WeatherForecastController » avec une méthode Get d’exemple déjà codée. Vous pouvez y jeter un oeil. Cerise sur le gâteau, notre API est déjà fonctionnelle ! Pour le vérifier, lancer directement le projet après avoir pris soin de vérifier votre API est sélectionnée dans la partie débug (IIS Express peut être sélectionné par défaut) : ![](https://i.imgur.com/ytoRkCo.png) > ℹ IIS Express est un serveur WEB intégré à Visual Studio et permet le debug local de l'application Après avoir lancer votre API, deuxième surprise ! ![](https://i.imgur.com/Brgycik.png) > Swagger (générateur de documentation + testeur d’API), est déjà préinstallé ! Vous pouvez tester votre méthode de Get du Contrôleur WeatherForecast : ![](https://i.imgur.com/ETHGkwd.png) ![](https://i.imgur.com/F9Vm6fh.png) > Pour plus d’informations sur Swagger, rendez-vous ici : https://docs.microsoft.com/fr-fr/aspnet/core/tutorials/web-api-help-pages-using-swagger?view=aspnetcore-6.0 --- ## Base de données locale Une API qui répond avec des données fictives c’est bien, mais avoir une vraie base de données derrière c’est encore mieux ! Commençons par créer physiquement notre base ! ### Vérifier l'instance Avec l’invite de commande, vérifions l’existence de l’instance LocalDB avec la commande ``` bat sqllocaldb info ``` ![](https://i.imgur.com/0aumVnJ.png) Vérifions qu’elle est démarrée avec : ``` bat sqllocaldb info MSSQLLocalDB ``` > Pour aller plus loin : >* Si l’instance n’est pas démarrée : `sqllocaldb start MSSQLLocalDB` >* Si vous souhaitez créer votre instance : `sqllocaldb create TineosLocalDB -s` (« -s » pour la démarrer directement) >* Si `sqllocaldb` n’est pas reconnu en tant que commande, il manque une option à l’installation de Visual Studio (voir plus haut dans la partie pré-requis) ### Création de la BDD Ouvrir SQL Server Managagement Studio (SSMS) et se connecter à la BDD ainsi : ![](https://i.imgur.com/42qtpEk.png) > Le préfixe « **(localdb)** » est obligatoire et invariable pour les bases locales comme la nôtre. > Ce qui suit correspond au nom de l’instance, dans notre cas « **MSSQLLocalDB** ». Une fois connecté, on crée notre base de données `TineosDatabase`: ![](https://i.imgur.com/gBk68MN.png) ![](https://i.imgur.com/WHExP8E.png) > Il n’y a maintenant plus rien à faire du côté de SSMS. ### Modélisation de la BDD Maintenant que notre base est créée physiquement, modélisons là sous Visual Studio avec l'aide d'Entity Framework. Afin de respecter un découpage fonctionnel, nous allons créer un projet dédié à notre base : ![](https://i.imgur.com/V6lpvI5.png) Celui-ci sera du type « Class Library » : ![](https://i.imgur.com/IgmkbgK.png) ![](https://i.imgur.com/II7z5ON.png) Vérifions que la version de .NET est bien .NET 6 ![](https://i.imgur.com/3A8iajE.png) ![](https://i.imgur.com/nZcdEfi.png) > Class1.cs est à supprimer, sauf s’il est important pour vous de garder le 1er vestige de votre 1ère API .NET 6 pour des raisons sentimentales, libre à vous ! 😝). Référençons Entity Framework dans nos projets. Pour cela nous allons utiliser le gestionnaire des packages NuGet : ![](https://i.imgur.com/3NU6C5i.png) > NuGet est un outil de gestion de bibliothèques intégré directement dans Visual Studio. Il nous évitera notamment de charger les DLL manuellement et gère aussi le versionning de celles-ci. Pour plus d'information sur le sujet, rendez-vous ici : https://docs.microsoft.com/fr-fr/nuget/consume-packages/install-use-packages-visual-studio Installons-les packages suivants, dans leur dernière version, **dans le projet Database uniquement** : * `Microsoft.AspNetCore.Identity.EntityFrameworkCore` * `Microsoft.EntityFrameworkCore` * `Microsoft.EntityFrameworkCore.SqlServer` * `Microsoft.EntityFrameworkCore.Tools` Installons le package suivant, dans sa dernière version, **dans les deux projets** : * `Microsoft.EntityFrameworkCore.Design` (le projet API, étant le point d'entrée de notre application, a besoin de cette référence pour générer notre base) Créons notre tout premier Model Entity en ajoutant une nouvelle classe `Tineos.cs` dans le projet `Database/EntityModels` : ![](https://i.imgur.com/egKaFbO.png) > Cette classe servira à créer la table « Tineos » Ajoutons maintenant quelques propriétés à notre classe et rendons la publique : ```cs public class Tineos { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Mail { get; set; } public string Password { get; set; } public DateTime StartDate { get; set; } public string JobFunction { get; set; } } ``` > Ces propriétés deviendront les champs de la future table « Tineos » > > **Remarques** : > * Par défault, Entity Framework reconnaitra automatiquement les propriétés « Id » ou « ID » ou encore « TineosId » comme la clé primaire de la future table. > * De plus si le type de cette propriété est int, une valeur incrémentale sera gérée automatiquement du côté de la base de données lors des ajouts. Il est bien sûr possible de modifier ce comportement. Pour en savoir plus, rendez-vous ici : https://docs.microsoft.com/fr-fr/ef/core/modeling/generated-properties?tabs=data-annotations Créons maintenant le contexte de notre base de données. Il s'agit du point d’entrée de nos données au niveau du code. Nous devons lister ici tous les modèles Entity que l’on souhaite rendre accessible par le biais du contexte. Créons la classe `DatabaseContext.cs` à la racine du projet Database : ![](https://i.imgur.com/aCmMtvi.png) ```cs public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) {} public DbSet<Tineos> Tineos {get; set;} } ``` > * Notre classe doit étendre `Microsoft.EntityFrameworkCore.DbContext`, pour se raccrocher à Entity Framework. > * Il faut un constructeur permettant de passer des options au DbContext > * Et lister les collections des modèles par le biais de propriétés de type DbSet Renseignons notre chaîne de connexion dans le Projet API, dans `appsettings.Development.json` : ``` json "ConnectionStrings": { "TineosCnxStr": "Server=(localdb)\\MSSQLLocalDB;Initial Catalog=TineosDatabase;Integrated Security=true;MultipleActiveResultSets=True" } ``` ![](https://i.imgur.com/aY3t4rJ.png) > **Remarque** : `appsettings.Development.json` ne sera pris en compte uniquement dans le cas de notre machine local (ou toute autre machine où la variable d’environnement (VE) « ASPNETCORE_ENVIRONMENT » a pour valeur « Development ») Il est facile de jouer avec cette VE sur vos serveurs cibles pour variabiliser une chaîne de connexion, par exemple. Plus d’informations ici : https://docs.microsoft.com/fr-fr/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0 Pour que notre chaîne de connexion soit partagée dans toutes l'application, nous allons l'injecter par dépendance. Dans le `Program.cs` du projet API, ajouil suffit d'ajouter l’instruction suivante dans la méthode `ConfigureServices()` : ```cs builder.Services.AddDbContext<DatabaseContext>( options => options.UseSqlServer(builder.Configuration.GetConnectionString("TineosCnxStr"))); ``` ![](https://i.imgur.com/GbaJZos.png) > N’oubliez pas de référencer votre projet Database dans votre projet API afin de pouvoir référencer votre « DatabaseContext ». Cette configuration permettra d’injecter le contexte dans les classes voulues, en utilisant automatiquement la chaîne de connexion en paramètre du constructeur réalisé tout à l’heure. --- ## Génération des tables ### Les migrations Pour générer les modèles présents dans notre contexte, il faut créer une migration. Ouvrir le Package Manager Console de Visual Studio : ![](https://i.imgur.com/KKBa6Gx.png) Sélectionner le projet Database par défaut : ![](https://i.imgur.com/GDnHio2.png) Lancer la commande suivante : `Add-Migration InitialMigration` > Un dossier migration a été créé dans notre projet DataBase avec dedans notre fichier de migration (nommé YYYYDDJJHHMMSS_InitialMigration.cs). Pour mettre à jour la base, il faut lancer cette commande : `Update-Database` On peut observer sur SSMS que la base est à jour : ![](https://i.imgur.com/74PTHVX.png) > Pour chaque changement de modèles Entity (ajout, modification ou suppression), nous devons créer une nouvelle migration pour mettre à jour la base. Les scripts générés seront alors des différentiels par rapport à la dernière migration. ### Ajouter des tables à notre BDD existante Pour approfondir notre exemple, créons un nouveau modèle « Project » que nous allons lier à « Tineos » en many to many : ```cs public class Project { public int Id { get; set; } public string Name { get; set; } public DateTime StartDate { get; set; } public DateTime DeadLine { get; set; } public List<ProjectTineos> ProjectTineos { get; set; } } ``` Puis, l’entité d’association : ```cs public class ProjectTineos { public int TineosId { get; set; } public Tineos Tineos { get; set; } public int ProjectId { get; set; } public Project Project { get; set; } } ``` Précisons qu’un Tineos peut avoir une liste de projets en ajoutant cette propriété dans la classe `Tineos.cs` : ```cs public List<ProjectTineos> TineosProject { get; set; } ``` Et complètons les propriétés DatabaseContext et ajoutons la méthode `OnModelCreating()` qui va préciser les clés primaires de la table d’association et on en profite pour ajouter quelques données pour les tests : ```cs public class DatabaseContext : DbContext { public DatabaseContext(DbContextOptions<DatabaseContext> options) : base(options) { } public DbSet<Tineos> Tineos { get; set; } public DbSet<Project> Projects { get; set; } public DbSet<ProjectTineos> ProjectTineos { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<ProjectTineos>() .HasKey(s => new { s.ProjectId, s.TineosId }); var john = new Tineos { Id = 1, StartDate = DateTime.Now, FirstName = "John", LastName = "DOE", Mail = "john.doe@mail.com", Password = "password", JobFunction = "Communication Expert" }; var jeny = new Tineos { Id = 2, StartDate = DateTime.Now, FirstName = "Jeny", LastName = "ANDERSON", Mail = "jeny.anderson@mail.com", Password = "password", JobFunction = "Big Boss" }; var carl = new Tineos { Id = 3, StartDate = DateTime.Now, FirstName = "Carl", LastName = "WICK", Mail = "carl.wick@mail.com", Password = "password", JobFunction = "Lead Developer" }; var google = new Project { Id = 1, StartDate = DateTime.Now, DeadLine = DateTime.Now.AddYears(100), Name = "Buy Google" }; var piedPipper = new Project { Id = 2, StartDate = DateTime.Now, DeadLine = DateTime.Now.AddYears(2), Name = "Develop new Pied Pipper" }; var happy = new Project { Id = 3, StartDate = DateTime.Now, DeadLine = DateTime.Now, Name = "Be Happy" }; var assoGoogle1 = new ProjectTineos { ProjectId = google.Id, TineosId = john.Id }; var assoGoogle2 = new ProjectTineos { ProjectId = google.Id, TineosId = jeny.Id }; var assoPp = new ProjectTineos { ProjectId = piedPipper.Id, TineosId = carl.Id }; var assoHappy1 = new ProjectTineos { ProjectId = happy.Id, TineosId = john.Id }; var assoHappy2 = new ProjectTineos { ProjectId = happy.Id, TineosId = jeny.Id }; var assoHappy3 = new ProjectTineos { ProjectId = happy.Id, TineosId = carl.Id }; modelBuilder.Entity<Tineos>().HasData(john, jeny, carl); modelBuilder.Entity<Project>().HasData(google, happy, piedPipper); modelBuilder.Entity<ProjectTineos>().HasData(assoHappy1, assoHappy2, assoHappy3, assoPp, assoGoogle1, assoGoogle2); } } ``` > Ne créez pas au scandale tout de suite, les mots de passes en clair on va y revenir 👮‍♀️👮‍♂️ Créons une nouvelle migration (`Add-Migration AddingProjects`), mettons à jour la BDD (`Update-Database`) : ![](https://i.imgur.com/P3Lgw65.png) --- ## Requêter en base Afin de continuer notre découpage fonctionnel, créons un nouveau projet Class Library « TineosProject.Domain » et y ajouter un dossier Models, Interfaces et Business : ![](https://i.imgur.com/m7vX5MR.png) Dans Models, créons une classe TineosModel : ```cs public class TineosModel { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string JobFunction { get; set; } public string Mail { get; set; } public DateTime StartDate { get; set; } } ``` Dans `/Interfaces`, créons l’interface `ITineosManager.cs` et `ITineosBusiness.cs` comme ainsi : ![](https://i.imgur.com/vIptzGU.png) ```cs public interface ITineosManager { List<TineosModel> GetAllTineos(); } ``` ```cs public interface ITineosBusiness { List<TineosModel> GetAllTineos(); } ``` Dans `/Business`, créons `TineosBusiness.cs` : ```cs public class TineosBusiness : ITineosBusiness { public ITineosManager _tineosManager; public TineosBusiness(ITineosManager tineosManager) { _tineosManager = tineosManager; } public List<TineosModel> GetAllTineos() { return _tineosManager.GetAllTineos(); } } ``` > L’instance du manager sera passée par injection de dépendance. Nous allons la configurer un peu plus bas. Dans le projet **TineosProject.Database**, ajoutons la référence au projet **TineosProject.Domain** et ajoutons un dossier `/Managers` avec à l’intérieur la classe `TineosManager.cs` : ```cs public class TineosManager : ITineosManager { private readonly DatabaseContext _databaseContext; public TineosManager(DatabaseContext databaseContext) { _databaseContext = databaseContext; } public List<TineosModel> GetAllTineos() { return _databaseContext.Tineos.Select(t => new TineosModel { Id = t.Id, FirstName = t.FirstName, LastName = t.LastName, JobFunction = t.JobFunction }).ToList(); } } ``` > Le contexte sera également passée par injection de dépendance (déjà configurée précédemment). Direction le `Program.cs` du projet **TineosProject.API**, et ajouter la ligne suivante : ```cs builder.Services.AddScoped<ITineosManager, TineosManager>(); ``` ![](https://i.imgur.com/CkUDM3q.png) --- ## Interrogeons notre API ! Dans notre projet API, créons un nouveau contrôleur `Controller/TineosController.cs` : ```cs [ApiController] [Route("[controller]")] public class TineosController : ControllerBase { private static ITineosBusiness _tineosBusiness; public TineosController(ITineosBusiness tineosBusiness) { _tineosBusiness = tineosBusiness; } [HttpGet] public ActionResult Get() { return Ok(_tineosBusiness.GetAllTineos()); } } ``` De la même manière, on configure l’injection de dépendance dans le `Program.cs` : ```cs builder.Services.AddScoped<ITineosBusiness, TineosBusiness>(); ``` ![](https://i.imgur.com/faQxo8n.png) > Concernant l'injection de dépendance, il y a plusieurs cycles de vie possibles. Dans notre cas, nous utilisons "Scoped" afin que chaque requête HTTP utilise la même instance des objets passés en paramètre de constructeur. Il existe deux autres types : Transient (éphémère) et Singleton (pas besoin de l'expliquer celui-là, si ? 😋). Le choix se fait en fonction du rôle de ce que l'on souhaite injecter. Pour plus d'information sur le sujet, n'hésitez pas à lire cet article qui le résume déjà très bien : https://cdiese.fr/aspnet-core-dependency-injection/ On peut maintenant lancer le projet API et tester notre méthode `Get()` exposée : ![](https://i.imgur.com/zCwbYSC.png) Allons plus loin ! Dans le projet **TineosProject.Domain**, on ajoute une nouvelle signature dans l’interface `ITineosBusiness.cs` : ```cs public interface ITineosBusiness { List<TineosModel> GetAllTineos(); TineosModel AddTineos(AddTineosRequest request); } ``` Avec un modèle de requête dans `/Models/Requests/AddTineosRequest.cs` : ```cs public class AddTineosRequest { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } [Required] public DateTime StartDate { get; set; } [Required] public string JobFunction { get; set; } [Required] public string Mail { get; set; } [Required] public string Password { get; set; } } ``` > Il est possible de spécifier dans le modèle d’entrée des champs obligatoires (entre-autres) par le biais des DataAnnotations (exemple : [Required] ). Ces annotations serviront notamment à valider les données d’entrée de notre méthode de contrôleur. On continue notre chaîne dans `ITineosManager.cs` : ```cs public interface ITineosManager { List<TineosModel> GetAllTineos(); TineosModel AddTineos(TineosModel request , string clearPassword); } ``` Et on implémente la méthode dans la classe `TineosBusiness.cs` : ```cs public TineosModel AddTineos(AddTineosRequest request) { var tineosToAdd = new TineosModel { FirstName = request.FirstName, LastName = request.LastName, JobFunction = request.JobFunction, Mail = request.Mail, StartDate = request.StartDate }; return _tineosManager.AddTineos(tineosToAdd, request.Password); } ``` > **Remarques :** > * Ces méthodes sont pour l’instant des passe-plats dans la mesure où notre besoin est très simple. On pourrait imaginer qu’une méthode de la brique Business reçoit plusieurs paramètres et utilise plusieurs managers. > * Les objets passés par le contrôleur vers le business sont les mêmes que ceux envoyés du domaine vers le manager. Nous gardons cette simplicité aujourd’hui pour alléger cet article, mais il convient en principe décorréler également ces deux couches. Dans le projet **TineosProject.Database**, on implémente la nouvelle méthode dans `TineosManager.cs` : ```cs public TineosModel AddTineos(TineosModel tineosModel, string clearPassword) { var tineosToAdd = new Tineos { FirstName = tineosModel.FirstName, LastName = tineosModel.LastName, StartDate = DateTime.Now, JobFunction = tineosModel.JobFunction, Mail = tineosModel.Mail, Password = PasswordTool.HashPassword(clearPassword) }; _databaseContext.Add(tineosToAdd); _databaseContext.SaveChanges(); tineosModel.Id = tineosToAdd.Id; return tineosModel; } ``` > **Remarques :** > * Le mapping entre les objets pourrait être automatisé avec l’aide d’une librairie comme AutoMapper pour gagner en visibilité dans cette couche. > * Allez, cette fois-ci on en parle ! NON : Nous n’allions pas stocker le mot de passe en clair quand même 😊 PasswordTool.HashPassword va nous aider à hacher le mot de passe avant insertion. Voici un exemple de hachage créé dans un dossier `/Tools` du même projet : ```cs public static class PasswordTool { public static string HashPassword(string password) { byte[] salt = new byte[128 / 8]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(salt); } string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( password: password, salt: Encoding.UTF8.GetBytes("saltForPasswordHashing"), prf: KeyDerivationPrf.HMACSHA512, iterationCount: 10000, numBytesRequested: 256 / 8)); return hashed; } } ``` > Penser à changer le salt et le variabiliser 😉 > Aussi, n'hésitez pas à revenir sur vos données "seedées" dans le fichier `DatabaseContext.cs` pour là aussi hacher les mots de passe placé en dur un peu plus haut : >```cs >var john = new Tineos >{ > Id = 1, > StartDate = DateTime.Now, > FirstName = "John", > LastName = "DOE", > Mail = "john.doe@mail.com", > //Password = "password" --> #shame > Password = PasswordTool.HashPassword("password"), > JobFunction = "Communication Expert" >}; >``` On peut maintenant implémenter notre nouvelle méthode de contrôleur pour ajouter un nouveau Tineos : ```cs [HttpPost] public ActionResult Post(AddTineosRequest request) { if(ModelState.IsValid) { return Ok(_tineosBusiness.AddTineos(request)); } else { return BadRequest(ModelState); } } ``` > Un champ obligatoire non renseigné (le `[Required]` vu plus haut) aura pour conséquence d’invalider le test « ModelState.IsValid ». Dans notre cas, une BadRequest sera renvoyée avec le détail de l’erreur. Testons la nouvelle méthode : ![](https://i.imgur.com/GeRuu0q.png) ![](https://i.imgur.com/uX4EURr.png) On peut constater son ajout en base de données : ![](https://i.imgur.com/2nXktw3.png) --- ## Mise en place d’un token d’authentification JWT Commençons par installer les packages suivants dans le projet TineosProject.API : * `Microsoft.AspNetCore.Authentication` * `Microsoft.AspNetCore.Authentication.JwtBearer` * `Microsoft.IdentityModel.Tokens` Ensuite, créons une méthode de génération du Token JWT dans `TineosProject.Domain/Tools/TokenTool.cs` : ```cs public static class TokenTool { public static string GenerateJwt(TineosModel user, JwtSettings jwtSettings) { var claims = new List<Claim> { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), new Claim(ClaimTypes.Role, user.JobFunction) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512); var expires = DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings.ExpirationInMinutes)); var token = new JwtSecurityToken( issuer: jwtSettings.Issuer, audience: jwtSettings.Issuer, claims, expires: expires, signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } ``` Et créons ensuite la classe de configuration suivante `/Models/Config/JwtSettings.cs` : ```cs public class JwtSettings { public string Issuer { get; set; } public string Secret { get; set; } public int ExpirationInMinutes { get; set; } } ``` > Ainsi que la configuration liée dans le appsettings.Development.json dans le projet **TineosProject.API** (nous ferons le lien un peu plus bas): ```json "JwtSettings": { "Issuer": "MyTineosAPI", "Secret": "JwtS3cr3tK3yWithM@ximumSiz3Is64Byt3s", "ExpirationInMinutes": 60 } ``` Dans `Program.cs`, configurons la partie JWT : ```cs //configuration de l'authentification et du format de token var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>(); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = jwtSettings.Issuer, ValidAudience = jwtSettings.Issuer, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)), ClockSkew = TimeSpan.Zero }; }); app.UseAuthentication(); ``` Limitons maintenant l’utilisation de notre méthode POST en ajoutant l’attribut `[Authorize]` à celle-ci : ![](https://i.imgur.com/aYLpRTy.png) Testons maintenant l’accès à nos deux méthodes : * Pas de soucis pour la Méthode `Get()`: ![](https://i.imgur.com/Rn5rHuf.png) * On constate maintenant que notre méthode Post( ) est maintenant inaccessible : ![](https://i.imgur.com/V07W3nK.png) > Nous avons mis l’attribut `[Authorize]` aux dessus de notre méthode `Post()`, il est possible de mettre celui-ci directement au-dessus de la classe. Ainsi, le token sera demandé pour toutes les méthodes du contrôleur sauf mention spécifique contraire. Il est possible d’avoir une stratégie générale et d’ensuite venir affiner au cas par cas avec d’autres attributs sur une méthode en particulier. Un attribut `[AllowAnonymous]` autorisera son appel sans token, malgré le fait que la classe ait l’attribut Authorize. Pour en savoir plus, rendez-vous ici : https://docs.microsoft.com/fr-fr/aspnet/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api --- ## Création du Token Créons une nouvelle signature et méthode dans `ITineosManager.cs` et `TineosManager.cs` qui va permettre de vérifier la validité des données de connexion : ```cs public TineosModel AuthenticateTineos(string mail, string password) { TineosModel result = null; var tineos =_databaseContext.Tineos.Where(t => t.Mail == mail).FirstOrDefault(); if(tineos != null && tineos.Password == PasswordTool.HashPassword(password)) { result = new TineosModel { Id = tineos.Id, FirstName = tineos.FirstName, LastName = tineos.LastName, JobFunction = tineos.JobFunction, Mail = tineos.Mail, StartDate = tineos.StartDate }; } return result; } ``` Ajoutons maintenant une nouvelle interface `IAuthBusiness.cs` et une classe `AuthBusiness.cs` ainsi : ```cs public interface IAuthBusiness { TokenModel ConnectUser(ConnectUserRequest request); } ``` ```cs public class AuthBusiness : IAuthBusiness { public ITineosManager _tineosManager; private readonly IOptions<JwtSettings> _jwtSettings; public AuthBusiness(ITineosManager tineosManager, IOptions<JwtSettings> jwtSettings) { _tineosManager = tineosManager; _jwtSettings = jwtSettings; } public TokenModel ConnectUser(ConnectUserRequest request) { TokenModel result = null; var existingTineos = _tineosManager.AuthenticateTineos(request.Mail, request.Password); if (existingTineos != null) { result = new TokenModel { Mail = existingTineos.Mail, Token = TokenTool.GenerateJwt(existingTineos, _jwtSettings.Value) }; } return result; } } ``` Et les `/Models/Requests/Auth/ConnectUserRequest.cs` et `/Models/TokenModel.cs` correspondant : ```cs public class ConnectUserRequest { [Required] public string Mail { get; set; } [Required] public string Password { get; set; } } ``` ```cs public class TokenModel { public string Mail { get; set; } public string Token { get; set; } } ``` `ITineosManager` est déjà injecté par dépendance, mais ce n’est pas le cas de `JwtSettings`. Pour cela il faut pas oublier cette instruction dans le `Program.cs` : ``` builder.Services.Configure<JwtSettings>(builder.Configuration.GetSection("JwtSettings")); ``` Et créons maintenant un nouveau contrôleur `AuthController.cs` : ```cs [ApiController] [Route("[controller]")] public class AuthController : ControllerBase { private static IAuthBusiness _authBusiness; public AuthController(IAuthBusiness authBusiness) { _authBusiness = authBusiness; } [HttpPost] public ActionResult Login(ConnectUserRequest request) { if (ModelState.IsValid) { var token = _authBusiness.ConnectUser(request); return token != null ? Ok(token) : BadRequest("Invalid credentials"); } else { return BadRequest(ModelState); } } } ``` Et toujours la même chanson, on oublie pas cette ligne dans le `Program.cs` pour injecter `ITineosBusiness` : ```cs builder.Services.AddScoped<IAuthBusiness, AuthBusiness>(); ``` --- ## Test de l'authentification Configurons Swagger dans `Program.cs` pour qu’il puisse injecter un token JWT : ```cs //configuration de Swagger builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "TineosProject.API", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Scheme = "Bearer", BearerFormat = "JWT", Description = "Ajouter le token ainsi : \"Bearer xxxx\" où xxxx est votre token d'authentification", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] {} } }); }); ``` Testons maintenant la méthode d’authenfication de l’API avec un des comptes présents en base : ![](https://i.imgur.com/xyXnrqF.png) ![](https://i.imgur.com/SCQtukD.png) Nous allons maintenant utiliser le token reçu pour s'authentifier. Pour cela cliquons sur le bouton Authorize en haut à droite de la page Swagger: ![](https://i.imgur.com/8uqe6Qj.png) et renseignons notre token fraichement obtenu (ne pas oublier de mettre « Bearer » devant) : ![](https://i.imgur.com/QJjHhSZ.png) Après avoir cliqué sur Authorize, vous pouvez-maintenant tester la méthode Tineos/Post que nous avons protégée tout à l’heure : ![](https://i.imgur.com/6HjjLl1.png) ![](https://i.imgur.com/dVRZzgt.png) --- ## Affiner par rôle utilisateur Il est aussi possible de limiter l’accès à une méthode à un rôle en particulier. Si vous avez été attentif tout à l’heure, vous avez remarqué que le rôle est stocké dans le token 🙄. Il est possible de limiter l’accès à un (ou plusieurs) rôle spécifique ainsi : ![](https://i.imgur.com/mRSBE4F.png) Si nous testons avec le même utilisateur (John Doe, qui a le rôle Communication Expert) que précédement la méthode `/Tineos/Get`, nous obtenons : ![](https://i.imgur.com/orRRShs.png) Si nous récupérons un token avec le tineos Jeny Anderson (dont le rôle est Big Boss), nous obtenons maintenant : ![](https://i.imgur.com/3WwKXRE.png) --- ⚠ L'authentification proposée ici est simplifiée. En effet dans le cadre d'une application WEB, il convient de coupler un Token JWT avec un Refresh Token qui permet de maintenir une connexion. Sans quoi, dès que le Token JWT arrive à expiration, toutes les requêtes demandant une authentification seront non autorisées et provoqueront, fonctionnellement, une déconnexion de l'utilisateur (et si ce cas est bien géré 😁). Pour plus d'information sur le mécanisme d'un Resfresh Token, rendez-vous ici : https://jasonwatmore.com/post/2022/01/24/net-6-jwt-authentication-with-refresh-tokens-tutorial-with-example-api --- # Conclusion Nous arrivons à la fin de cet article. Voici un résumé des notions que vous venez d’aborder : * Création d’une Web API en .NET 6 avec une ébauche d’architecture séparant les différentes couches fonctionnelles de notre application (API, Métier et BDD) * Création et manipulation d’une base de données locale avec l’approche Entity Code First et LocalDB * Manipulation de l’injection de dépendance native .NET 6 * Sécurisation de l’API par Token JWT et par rôle J’espère que cet article vous aura permis d’y voir un peu plus clair sur la création d’un projet API .NET 6 from scratch. Le but était de rester simple et de proposer un Getting Started. Les ajustements sont maintenant à votre main, en fonction de vos besoins et contraintes : c’est à vous de coder ! En comparaison au précedent article (https://www.attineos.com/blog/tech/mise-en-place-dune-api-en-net-5), le "gros" changement se résume à la suppression du fichier `startup.cs` et tout a été rassemblé dans le `Program.cs` qui est maintenant plus simple. En effet, il n'est plus sous forme d'une classe mais plutôt une liste d'instruction permettant de configurer son appli WEB. Microsoft a toutefois prévu une rétrocompatibilité pour le fichier startup.cs. Donc si vous souhaitez migrer vers .NET 6, le changement ne sera pas obligatoire, pour l'instant 🙂 Pour de plus amples informations sur .NET 6, rendez-vous ici : https://docs.microsoft.com/fr-fr/dotnet/fundamentals/

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully