Angulaire 5 et ASP.NET Core

Publié: 2022-03-11

J'ai pensé à écrire un article de blog depuis que la première version d'Angular a pratiquement tué Microsoft du côté client. Des technologies comme ASP.Net, Web Forms et MVC Razor sont devenues obsolètes, remplacées par un framework JavaScript qui n'est pas exactement Microsoft. Cependant, depuis la deuxième version d'Angular, Microsoft et Google ont travaillé ensemble pour créer Angular 2, et c'est à ce moment-là que mes deux technologies préférées ont commencé à travailler ensemble.

Dans ce blog, je veux aider les gens à créer la meilleure architecture combinant ces deux mondes. Es-tu prêt? Nous y voilà!

À propos de l'architecture

Vous allez créer un client Angular 5 qui utilise un service RESTful Web API Core 2.

Le côté client :

  • Angulaire 5
  • CLI angulaire
  • Matériau angulaire

Côté serveur :

  • API Web .NET C# Core 2
  • Dépendances d'injection
  • Authentification JWT
  • Code de structure d'entité en premier
  • serveur SQL

Noter

Dans cet article de blog, nous supposons que le lecteur possède déjà des connaissances de base sur TypeScript, les modules angulaires, les composants et l'importation/exportation. Le but de cet article est de créer une bonne architecture qui permettra au code de se développer au fil du temps.

De quoi avez-vous besoin?

Commençons par choisir l'IDE. Bien sûr, ce n'est que ma préférence, et vous pouvez utiliser celui avec lequel vous vous sentez le plus à l'aise. Dans mon cas, j'utiliserai Visual Studio Code et Visual Studio 2017.

Pourquoi deux IDE différents ? Depuis que Microsoft a créé Visual Studio Code pour le front-end, je ne peux pas arrêter d'utiliser cet IDE. Quoi qu'il en soit, nous verrons également comment intégrer Angular 5 dans le projet de solution, cela vous aidera si vous êtes le genre de développeur qui préfère déboguer à la fois le back-end et le front avec un seul F5.

Concernant le back-end, vous pouvez installer la dernière version de Visual Studio 2017 qui a une édition gratuite pour les développeurs mais qui est très complète : Community.

Donc, voici la liste des choses que nous devons installer pour ce tutoriel :

  • Code Visual Studio
  • Communauté Visual Studio 2017 (ou autre)
  • Node.js v8.10.0
  • Serveur SQL 2017

Noter

Vérifiez que vous exécutez au moins Node 6.9.x et npm 3.xx en exécutant node -v et npm -v dans une fenêtre de terminal ou de console. Les versions plus anciennes produisent des erreurs, mais les versions plus récentes conviennent.

Le frontal

Démarrage rapide

Que la fête commence! La première chose que nous devons faire est d'installer Angular CLI globalement, alors ouvrez l'invite de commande node.js et exécutez cette commande :

 npm install -g @angular/cli

Bon, maintenant nous avons notre groupeur de modules. Cela installe généralement le module sous votre dossier utilisateur. Un alias ne devrait pas être nécessaire par défaut, mais si vous en avez besoin, vous pouvez exécuter la ligne suivante :

 alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"

L'étape suivante consiste à créer le nouveau projet. Je l'appellerai angular5-app . Tout d'abord, nous naviguons vers le dossier dans lequel nous voulons créer le site, puis :

 ng new angular5-app

Première construction

Bien que vous puissiez tester votre nouveau site Web en exécutant simplement ng serve --open , je vous recommande de tester le site à partir de votre service Web préféré. Pourquoi? Eh bien, certains problèmes ne peuvent survenir qu'en production, et la construction du site avec ng build est le moyen le plus proche d'aborder cet environnement. Ensuite, nous pouvons ouvrir le dossier angular5-app avec Visual Studio Code et exécuter ng build sur le terminal bash :

construire l'application angulaire pour la première fois

Un nouveau dossier appelé dist sera créé et nous pourrons le servir en utilisant IIS ou le serveur Web de votre choix. Ensuite, vous pouvez taper l'URL dans le navigateur, et... c'est fait !

la nouvelle structure de répertoire

Noter

Ce n'est pas le but de ce tutoriel de montrer comment configurer un serveur Web, donc je suppose que vous avez déjà cette connaissance.

Écran de bienvenue angulaire 5

Le dossier src

La structure du dossier src

Mon dossier src est structuré comme suit : Dans le dossier app , nous avons des components où nous allons créer pour chaque composant Angular les fichiers css , ts , spec et html . Nous allons également créer un dossier de config pour conserver la configuration du site, les directives contiendront toutes nos directives personnalisées, les helpers hébergeront le code commun comme le gestionnaire d'authentification, la mise en layout contiendra les principaux composants tels que le corps, la tête et les panneaux latéraux, les models conserveront ce qui correspondent aux modèles de vue back-end, et enfin services auront le code pour tous les appels vers le back-end.

En dehors du dossier de l' app , nous conserverons les dossiers créés par défaut, comme les assets et environments , ainsi que les fichiers racine.

Création du fichier de configuration

Créons un fichier config.ts dans notre dossier config et appelons la classe AppConfig . C'est là que nous pouvons définir toutes les valeurs que nous utiliserons à différents endroits de notre code ; par exemple, l'URL de l'API. Notez que la classe implémente une propriété get qui reçoit, en paramètre, une structure clé/valeur et une méthode simple pour accéder à la même valeur. De cette façon, il sera facile d'obtenir les valeurs en appelant simplement this.config.setting['PathAPI'] des classes qui en héritent.

 import { Injectable } from '@angular/core'; @Injectable() export class AppConfig { private _config: { [key: string]: string }; constructor() { this._config = { PathAPI: 'http://localhost:50498/api/' }; } get setting():{ [key: string]: string } { return this._config; } get(key: any) { return this._config[key]; } };

Matériau angulaire

Avant de commencer la mise en page, configurons le cadre du composant d'interface utilisateur. Bien sûr, vous pouvez en utiliser d'autres comme Bootstrap, mais si vous aimez le style de Material, je le recommande car il est également pris en charge par Google.

Pour l'installer, il nous suffit d'exécuter les trois commandes suivantes, que nous pouvons exécuter sur le terminal Visual Studio Code :

 npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs

La deuxième commande est due au fait que certains composants de matériau dépendent des animations angulaires. Je recommande également de lire la page officielle pour comprendre quels navigateurs sont pris en charge et ce qu'est un polyfill.

La troisième commande est due au fait que certains composants Material reposent sur HammerJS pour les gestes.

Nous pouvons maintenant procéder à l'importation des modules de composants que nous voulons utiliser dans notre fichier app.module.ts :

 import {MatButtonModule, MatCheckboxModule} from '@angular/material'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSidenavModule} from '@angular/material/sidenav'; // ... @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, MatButtonModule, MatCheckboxModule, MatInputModule, MatFormFieldModule, MatSidenavModule, AppRoutingModule, HttpClientModule ],

L'étape suivante consiste à modifier le fichier style.css , en ajoutant le type de thème que vous souhaitez utiliser :

 @import "~@angular/material/prebuilt-themes/deeppurple-amber.css";

Importez maintenant HammerJS en ajoutant cette ligne dans le fichier main.ts :

 import 'hammerjs';

Et enfin, tout ce qui nous manque, c'est d'ajouter les icônes Material à index.html , dans la section head :

 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

La disposition

Dans cet exemple, nous allons créer une mise en page simple comme celle-ci :

Exemple de mise en page

L'idée est d'ouvrir/masquer le menu en cliquant sur un bouton sur l'en-tête. Angular Responsive fera le reste du travail pour nous. Pour ce faire, nous allons créer un dossier de layout en page et y placer les fichiers app.component créés par défaut. Mais nous allons également créer les mêmes fichiers pour chaque section de la mise en page comme vous pouvez le voir dans l'image suivante. Ensuite, app.component sera le corps, head.component l'en-tête et left-panel.component le menu.

Dossier de configuration en surbrillance

Modifions maintenant app.component.html comme suit :

 <div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>

Fondamentalement, nous aurons une propriété d' authentication dans le composant qui nous permettra de supprimer l'en-tête et le menu si l'utilisateur n'est pas connecté, et d'afficher à la place une simple page de connexion.

Le head.component.html ressemble à ceci :

 <h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>

Juste un bouton pour déconnecter l'utilisateur, nous y reviendrons plus tard. Quant à left-panel.component.html , pour l'instant changez simplement le HTML en :

 <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>

Nous avons fait simple : jusqu'à présent, il n'y a que deux liens pour naviguer à travers deux pages différentes. (Nous y reviendrons également plus tard.)

Maintenant, voici à quoi ressemblent les fichiers TypeScript de la tête et du composant de gauche :

 import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }
 import { Component } from '@angular/core'; @Component({ selector: 'app-left-panel', templateUrl: './left-panel.component.html', styleUrls: ['./left-panel.component.css'] }) export class LeftPanelComponent { title = 'Angular 5 Seed'; }

Mais qu'en est-il du code TypeScript pour app.component ? Nous allons laisser un peu de mystère ici et le mettre en pause pendant un moment, et y revenir après avoir implémenté l'authentification.

Routage

Bon, maintenant nous avons Angular Material qui nous aide avec l'interface utilisateur et une mise en page simple pour commencer à construire nos pages. Mais comment naviguer entre les pages ?

Afin de créer un exemple simple, créons deux pages : "Utilisateur", où nous pouvons obtenir une liste des utilisateurs existants dans la base de données, et "Tableau de bord", une page où nous pouvons afficher des statistiques.

Dans le dossier de l' app , nous allons créer un fichier appelé app-routing.modules.ts ressemblant à ceci :

 import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './helpers/canActivateAuthGuard'; import { LoginComponent } from './components/login/login.component'; import { LogoutComponent } from './components/login/logout.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { UsersComponent } from './components/users/users.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full', canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, { path: 'logout', component: LogoutComponent}, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: 'users', component: UsersComponent,canActivate: [AuthGuard] } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}

C'est aussi simple que cela : en important simplement RouterModule et Routes depuis @angular/router , nous pouvons mapper les chemins que nous voulons implémenter. Ici, nous créons quatre chemins :

  • /dashboard : Notre page d'accueil
  • /login : La page où l'utilisateur peut s'authentifier
  • /logout : Un chemin simple pour déconnecter l'utilisateur
  • /users : Notre première page où nous voulons lister les utilisateurs du back-end

Notez que le tableau de dashboard est notre page par défaut, donc si l'utilisateur tape l'URL / , la page redirigera automatiquement vers cette page. Jetez également un œil au paramètre canActivate : Ici, nous créons une référence à la classe AuthGuard , qui nous permettra de vérifier si l'utilisateur est connecté. Sinon, il redirige vers la page de connexion. Dans la section suivante, je vais vous montrer comment créer cette classe.

Maintenant, tout ce que nous devons faire est de créer le menu. Vous vous souvenez de la section mise en page lorsque nous avons créé le fichier left-panel.component.html pour qu'il ressemble à ceci ?

 <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>

C'est ici que notre code rencontre la réalité. Nous pouvons maintenant créer le code et le tester dans l'URL : vous devriez pouvoir naviguer de la page Tableau de bord vers Utilisateurs, mais que se passe-t-il si vous saisissez directement l'URL our.site.url/users dans le navigateur ?

texte alternatif de l'image

Notez que cette erreur apparaît également si vous actualisez le navigateur après avoir déjà navigué avec succès vers cette URL via le panneau latéral de l'application. Pour comprendre cette erreur, permettez-moi de me référer à la doc officielle où c'est vraiment clair :

Une application routée doit prendre en charge les liens profonds. Un lien profond est une URL qui spécifie un chemin vers un composant à l'intérieur de l'application. Par exemple, http://www.mysite.com/users/42 est un lien profond vers la page de détail du héros qui affiche le héros avec l'identifiant : 42.

Il n'y a aucun problème lorsque l'utilisateur accède à cette URL à partir d'un client en cours d'exécution. Le routeur angulaire interprète l'URL et les routes vers cette page et ce héros.

Mais cliquer sur un lien dans un e-mail, le saisir dans la barre d'adresse du navigateur ou simplement actualiser le navigateur sur la page de détail du héros - toutes ces actions sont gérées par le navigateur lui-même, en dehors de l'application en cours d'exécution. Le navigateur fait une demande directe au serveur pour cette URL, en contournant le routeur.

Un serveur statique renvoie régulièrement index.html lorsqu'il reçoit une requête pour http://www.mysite.com/ . Mais il rejette http://www.mysite.com/users/42 et renvoie une erreur 404 - Not Found à moins qu'il ne soit configuré pour renvoyer index.html à la place.

Pour résoudre ce problème, c'est très simple, il suffit de créer la configuration du fichier du fournisseur de services. Puisque je travaille avec IIS ici, je vais vous montrer comment le faire dans cet environnement, mais le concept est similaire pour Apache ou tout autre serveur Web.

Nous créons donc un fichier à l'intérieur du dossier src appelé web.config qui ressemble à ceci :

 <?xml version="1.0"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Angular Routes" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> </system.webServer> <system.web> <compilation debug="true"/> </system.web> </configuration>

Ensuite, nous devons être sûrs que cet actif sera copié dans le dossier déployé. Tout ce que nous avons à faire est de modifier notre fichier de paramètres CLI angular-cli.json :

 { "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular5-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico", "web.config" // or whatever equivalent is required by your web server ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }

Authentification

Vous souvenez-vous comment nous avons fait implémenter la classe AuthGuard pour définir la configuration du routage ? Chaque fois que nous naviguons vers une page différente, nous utiliserons cette classe pour vérifier si l'utilisateur est authentifié avec un jeton. Sinon, nous redirigerons automatiquement vers la page de connexion. Le fichier pour cela est canActivateAuthGuard.ts — créez-le dans le dossier helpers et faites-le ressembler à ceci :

 import { CanActivate, Router } from '@angular/router'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Helpers } from './helpers'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router, private helper: Helpers) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { if (!this.helper.isAuthenticated()) { this.router.navigate(['/login']); return false; } return true; } }

Ainsi, chaque fois que nous changeons de page, la méthode canActivate sera appelée, ce qui vérifiera si l'utilisateur est authentifié, et si ce n'est pas le cas, nous utilisons notre instance de Router pour rediriger vers la page de connexion. Mais quelle est cette nouvelle méthode sur la classe Helper ? Sous le dossier helpers , créons un fichier helpers.ts . Ici, nous devons gérer localStorage , où nous stockerons le jeton que nous recevons du back-end.

Noter

En ce qui concerne localStorage , vous pouvez également utiliser des cookies ou sessionStorage , et la décision dépendra du comportement que nous souhaitons mettre en œuvre. Comme son nom l'indique, sessionStorage n'est disponible que pendant la durée de la session du navigateur et est supprimé lorsque l'onglet ou la fenêtre est fermé(e) ; il survit cependant aux rechargements de page. Si les données que vous stockez doivent être disponibles en permanence, alors localStorage est préférable à sessionStorage . Les cookies sont principalement destinés à la lecture côté serveur, tandis que localStorage ne peut être lu que côté client. La question est donc, dans votre application, qui a besoin de ces données --- le client ou le serveur ?


 import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs/Subject'; @Injectable() export class Helpers { private authenticationChanged = new Subject<boolean>(); constructor() { } public isAuthenticated():boolean { return (!(window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '')); } public isAuthenticationChanged():any { return this.authenticationChanged.asObservable(); } public getToken():any { if( window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '') { return ''; } let obj = JSON.parse(window.localStorage['token']); return obj.token; } public setToken(data:any):void { this.setStorageToken(JSON.stringify(data)); } public failToken():void { this.setStorageToken(undefined); } public logout():void { this.setStorageToken(undefined); } private setStorageToken(value: any):void { window.localStorage['token'] = value; this.authenticationChanged.next(this.isAuthenticated()); } }

Notre code d'authentification a-t-il un sens maintenant ? Nous reviendrons sur la classe Subject plus tard, mais pour l'instant revenons un instant sur la configuration du routage. Jetez un oeil à cette ligne:

 { path: 'logout', component: LogoutComponent},

C'est notre composant pour se déconnecter du site, et c'est juste une simple classe pour nettoyer le localStorage . Créons-le sous le dossier components/login avec le nom logout.component.ts :

 import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-logout', template:'<ng-content></ng-content>' }) export class LogoutComponent implements OnInit { constructor(private router: Router, private helpers: Helpers) { } ngOnInit() { this.helpers.logout(); this.router.navigate(['/login']); } }

Ainsi, chaque fois que nous allons à l'URL /logout , le localStorage sera supprimé et le site sera redirigé vers la page de connexion. Enfin, créons login.component.ts comme ceci :

 import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TokenService } from '../../services/token.service'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: [ './login.component.css' ] }) export class LoginComponent implements OnInit { constructor(private helpers: Helpers, private router: Router, private tokenService: TokenService) { } ngOnInit() { } login(): void { let authValues = {"Username":"pablo", "Password":"secret"}; this.tokenService.auth(authValues).subscribe(token => { this.helpers.setToken(token); this.router.navigate(['/dashboard']); }); } }

Comme vous pouvez le voir, pour le moment, nous avons codé en dur nos informations d'identification ici. Notez qu'ici nous appelons une classe de service ; nous allons créer ces classes de services pour accéder à notre back-end dans la section suivante.

Enfin, nous devons revenir au fichier app.component.ts , la mise en page du site. Ici, si l'utilisateur est authentifié, il affichera les sections de menu et d'en-tête, mais sinon, la mise en page changera pour afficher uniquement notre page de connexion.

 export class AppComponent implements AfterViewInit { subscription: Subscription; authentication: boolean; constructor(private helpers: Helpers) { } ngAfterViewInit() { this.subscription = this.helpers.isAuthenticationChanged().pipe( startWith(this.helpers.isAuthenticated()), delay(0)).subscribe((value) => this.authentication = value ); } title = 'Angular 5 Seed'; ngOnDestroy() { this.subscription.unsubscribe(); } }

Vous souvenez-vous de la classe Subject dans notre classe d'assistance ? Il s'agit d'un Observable . Observable prennent en charge la transmission de messages entre les éditeurs et les abonnés dans votre application. Chaque fois que le jeton d'authentification change, la propriété authentication est mise à jour. En examinant le fichier app.component.html , cela aura probablement plus de sens maintenant :

 <div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>

Prestations de service

À ce stade, nous naviguons sur différentes pages, authentifions notre côté client et rendons une mise en page très simple. Mais comment pouvons-nous obtenir des données du back-end ? Je recommande fortement de faire tous les accès back-end à partir des classes de service en particulier. Notre premier service sera dans le dossier services , appelé token.service.ts :

 import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { AppConfig } from '../config/config'; import { BaseService } from './base.service'; import { Token } from '../models/token'; import { Helpers } from '../helpers/helpers'; @Injectable() export class TokenService extends BaseService { private pathAPI = this.config.setting['PathAPI']; public errorMessage: string; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } auth(data: any): any { let body = JSON.stringify(data); return this.getToken(body); } private getToken (body: any): Observable<any> { return this.http.post<any>(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }

Le premier appel au serveur principal est un appel POST à ​​l'API de jeton. L'API de jeton n'a pas besoin de la chaîne de jeton dans l'en-tête, mais que se passe-t-il si nous appelons un autre point de terminaison ? Comme vous pouvez le voir ici, TokenService (et les classes de service en général) héritent de la classe BaseService . Jetons un coup d'oeil à ceci :

 import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Helpers } from '../helpers/helpers'; @Injectable() export class BaseService { constructor(private helper: Helpers) { } public extractData(res: Response) { let body = res.json(); return body || {}; } public handleError(error: Response | any) { // In a real-world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } public header() { let header = new HttpHeaders({ 'Content-Type': 'application/json' }); if(this.helper.isAuthenticated()) { header = header.append('Authorization', 'Bearer ' + this.helper.getToken()); } return { headers: header }; } public setToken(data:any) { this.helper.setToken(data); } public failToken(error: Response | any) { this.helper.failToken(); return this.handleError(Response); } }

Ainsi, chaque fois que nous effectuons un appel HTTP, nous implémentons l'en-tête de la requête en utilisant simplement super.header . Si le jeton est dans localStorage , il sera ajouté à l'intérieur de l'en-tête, mais sinon, nous définirons simplement le format JSON. Une autre chose que nous pouvons voir ici est ce qui se passe si l'authentification échoue.

Le composant de connexion appellera la classe de service et la classe de service appellera le back-end. Une fois que nous avons le jeton, la classe d'assistance gérera le jeton, et maintenant nous sommes prêts à obtenir la liste des utilisateurs de notre base de données.

Pour obtenir des données de la base de données, assurez-vous d'abord que nous faisons correspondre les classes de modèles avec les modèles de vue back-end dans notre réponse.

Dans user.ts :

 export class User { id: number; name: string; }

Et nous pouvons maintenant créer le fichier user.service.ts :

 import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { BaseService } from './base.service'; import { User } from '../models/user'; import { AppConfig } from '../config/config'; import { Helpers } from '../helpers/helpers'; @Injectable() export class UserService extends BaseService { private pathAPI = this.config.setting['PathAPI']; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } /** GET heroes from the server */ getUsers (): Observable<User[]> { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }

L'arrière-plan

Démarrage rapide

Bienvenue dans la première étape de notre application Web API Core 2. La première chose dont nous avons besoin est de créer une application Web ASP.Net Core, que nous appellerons SeedAPI.Web.API .

Création d'un nouveau fichier

Assurez-vous de choisir le modèle Vide pour un démarrage propre comme vous pouvez le voir ci-dessous :

choisissez le modèle vide

C'est tout, nous créons la solution en partant d'une application web vide. Maintenant, notre architecture sera telle que nous la listons ci-dessous, nous devrons donc créer les différents projets :

notre architecture actuelle

Pour ce faire, pour chacun d'entre eux, faites un clic droit sur la solution et ajoutez un projet "Bibliothèque de classes (.NET Core)".

ajouter une "Bibliothèque de classes (.NET Core)"

L'architecture

Dans la section précédente, nous avons créé huit projets, mais à quoi servent-ils ? Voici une description simple de chacun :

  • Web.API : Il s'agit de notre projet de démarrage et où les points de terminaison sont créés. Ici, nous allons configurer JWT, les dépendances d'injection et les contrôleurs.
  • ViewModels : Ici, nous effectuons des conversions à partir du type de données que les contrôleurs renverront dans les réponses au frontal. Il est recommandé de faire correspondre ces classes avec les modèles front-end.
  • Interfaces : Cela sera utile pour implémenter les dépendances d'injection. L'avantage incontestable d'un langage à typage statique est que le compilateur peut aider à vérifier qu'un contrat sur lequel repose votre code est réellement respecté.
  • Commons : Tous les comportements partagés et le code utilitaire seront ici.
  • Models : il est recommandé de ne pas faire correspondre la base de données directement avec les ViewModels front-end, donc le but des Models est de créer des classes de base de données d'entités indépendantes du front-end. Cela nous permettra à l'avenir de faire évoluer notre base de données sans forcément impacter notre front end. Cela aide également lorsque nous voulons simplement faire une refactorisation.
  • Maps : C'est ici que nous mappons les ViewModels aux Models et vice-versa. Cette étape est appelée entre les contrôleurs et les services.
  • Services : Une librairie pour stocker toute la logique métier.
  • Repositories : C'est le seul endroit où nous appelons la base de données.

Les références ressembleront à ceci :

Schéma des références

Authentification basée sur JWT

Dans cette section, nous verrons la configuration de base de l'authentification par jeton et approfondirons un peu le sujet de la sécurité.

Pour commencer à définir le jeton Web JSON (JWT), créons la classe suivante dans le dossier App_Start appelé JwtTokenConfig.cs . Le code à l'intérieur ressemblera à ceci :

 namespace SeedAPI.Web.API.App_Start { public class JwtTokenConfig { public static void AddAuthentication(IServiceCollection services, IConfiguration configuration) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = configuration["Jwt:Issuer"], ValidAudience = configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])) }; services.AddCors(); }); } } }

Les valeurs des paramètres de validation dépendront des exigences de chaque projet. L'utilisateur et l'audience valides que nous pouvons définir en lisant le fichier de configuration appsettings.json :

 "Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }

Ensuite, nous n'avons qu'à l'appeler depuis la méthode ConfigureServices dans startup.cs :

 // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }

Nous sommes maintenant prêts à créer notre premier contrôleur appelé TokenController.cs . La valeur que nous avons définie dans appsettings.json sur "veryVerySecretKey" doit correspondre à celle que nous utilisons pour créer le jeton, mais d'abord, créons le LoginViewModel dans notre projet ViewModels :

 namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }

Et enfin le contrôleur :

 namespace SeedAPI.Web.API.Controllers { [Route("api/Token")] public class TokenController : Controller { private IConfiguration _config; public TokenController(IConfiguration config) { _config = config; } [AllowAnonymous] [HttpPost] public dynamic Post([FromBody]LoginViewModel login) { IActionResult response = Unauthorized(); var user = Authenticate(login); if (user != null) { var tokenString = BuildToken(user); response = Ok(new { token = tokenString }); } return response; } private string BuildToken(UserViewModel user) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_config["Jwt:Issuer"], _config["Jwt:Issuer"], expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } private UserViewModel Authenticate(LoginViewModel login) { UserViewModel user = null; if (login.username == "pablo" && login.password == "secret") { user = new UserViewModel { name = "Pablo" }; } return user; } } }

La méthode BuildToken créera le jeton avec le code de sécurité donné. La méthode Authenticate n'a pour le moment que la validation de l'utilisateur codée en dur, mais nous devrons appeler la base de données pour la valider à la fin.

Le contexte d'application

La configuration d'Entity Framework est vraiment facile depuis que Microsoft a lancé la version Core 2.0 - EF Core 2 en abrégé. Nous allons approfondir avec un modèle code-first utilisant identityDbContext , alors assurez-vous d'abord d'avoir installé toutes les dépendances. Vous pouvez utiliser NuGet pour le gérer :

Obtenir des dépendances

En utilisant le projet Models , nous pouvons créer ici dans le dossier Context deux fichiers, ApplicationContext.cs et IApplicationContext.cs . De plus, nous aurons besoin d'une classe EntityBase .

Des classes

Les fichiers EntityBase seront hérités par chaque modèle d'entité, mais User.cs est une classe d'identité et la seule entité qui héritera de IdentityUser . Ci-dessous les deux classes :

 namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } }
 namespace SeedAPI.Models.EntityBase { public class EntityBase { public DateTime? Created { get; set; } public DateTime? Updated { get; set; } public bool Deleted { get; set; } public EntityBase() { Deleted = false; } public virtual int IdentityID() { return 0; } public virtual object[] IdentityID(bool dummy = true) { return new List<object>().ToArray(); } } }

Nous sommes maintenant prêts à créer ApplicationContext.cs , qui ressemblera à ceci :

 namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext<User>, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet<User> UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet<T> Set<T>() where T : class { return base.Set<T>(); } public void BeginTransaction() { dbContextTransaction = Database.BeginTransaction(); } public void CommitTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Commit(); } } public void RollbackTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Rollback(); } } public void DisposeTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Dispose(); } } } }

Nous sommes très proches, mais nous devrons d'abord créer plus de classes, cette fois dans le dossier App_Start situé dans le projet Web.API . La première classe consiste à initialiser le contexte de l'application et la seconde consiste à créer des exemples de données uniquement à des fins de test pendant le développement.

 namespace SeedAPI.Web.API.App_Start { public class DBContextConfig { public static void Initialize(IConfiguration configuration, IHostingEnvironment env, IServiceProvider svp) { var optionsBuilder = new DbContextOptionsBuilder(); if (env.IsDevelopment()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsStaging()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsProduction()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); var context = new ApplicationContext(optionsBuilder.Options); if(context.Database.EnsureCreated()) { IUserMap service = svp.GetService(typeof(IUserMap)) as IUserMap; new DBInitializeConfig(service).DataTest(); } } public static void Initialize(IServiceCollection services, IConfiguration configuration) { services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); } } }
 namespace SeedAPI.Web.API.App_Start { public class DBInitializeConfig { private IUserMap userMap; public DBInitializeConfig (IUserMap _userMap) { userMap = _userMap; } public void DataTest() { Users(); } private void Users() { userMap.Create(new UserViewModel() { id = 1, name = "Pablo" }); userMap.Create(new UserViewModel() { id = 2, name = "Diego" }); } } }

And we call them from our startup file:

 // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); } // ... // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider svp) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } DBContextConfig.Initialize(Configuration, env, svp); app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); app.UseAuthentication(); app.UseMvc(); }

Injection de dépendance

It is a good practice to use dependency injection to move among different projects. This will help us to communicate between controllers and mappers, mappers and services, and services and repositories.

Inside the folder App_Start we will create the file DependencyInjectionConfig.cs and it will look like this:

 namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped<IApplicationContext, ApplicationContext>(); services.AddScoped<IUserMap, UserMap>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserRepository, UserRepository>(); } } } 

image alt text

We will need to create for each new entity a new Map , Service , and Repository , and match them to this file. Then we just need to call it from the startup.cs file:

 // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }

Finally, when we need to get the users list from the database, we can create a controller using this dependency injection:

 namespace SeedAPI.Web.API.Controllers { [Route("api/[controller]")] [Authorize] public class UserController : Controller { IUserMap userMap; public UserController(IUserMap map) { userMap = map; } // GET api/user [HttpGet] public IEnumerable<UserViewModel> Get() { return userMap.GetAll(); ; } // GET api/user/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/user [HttpPost] public void Post([FromBody]string user) { } // PUT api/user/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string user) { } // DELETE api/user/5 [HttpDelete("{id}")] public void Delete(int id) { } } }

Look how the Authorize attribute is present here to be sure that the front end has logged in and how dependency injection works in the constructor of the class.

We finally have a call to the database but first, we need to understand the Map project.

Le projet de Maps

Cette étape consiste simplement à mapper les ViewModels vers et depuis les modèles de base de données. Nous devons en créer un pour chaque entité, et, suivant notre exemple précédent, le fichier UserMap.cs ressemblera à ceci :

 namespace SeedAPI.Maps { public class UserMap : IUserMap { IUserService userService; public UserMap(IUserService service) { userService = service; } public UserViewModel Create(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return DomainToViewModel(userService.Create(user)); } public bool Update(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return userService.Update(user); } public bool Delete(int id) { return userService.Delete(id); } public List<UserViewModel> GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List<UserViewModel> DomainToViewModel(List<User> domain) { List<UserViewModel> model = new List<UserViewModel>(); foreach (User of in domain) { model.Add(DomainToViewModel(of)); } return model; } public User ViewModelToDomain(UserViewModel officeViewModel) { User domain = new User(); domain.Name = officeViewModel.name; return domain; } } }

Il semble qu'une fois de plus, l'injection de dépendances fonctionne dans le constructeur de la classe, liant Maps au projet Services.

Le projet Services

Il n'y a pas grand-chose à dire ici : notre exemple est vraiment simple et nous n'avons pas de logique métier ou de code à écrire ici. Ce projet s'avérerait utile dans les futures exigences avancées lorsque nous devons calculer ou faire de la logique avant ou après les étapes de la base de données ou du contrôleur. En suivant l'exemple, la classe aura l'air assez nue :

 namespace SeedAPI.Services { public class UserService : IUserService { private IUserRepository repository; public UserService(IUserRepository userRepository) { repository = userRepository; } public User Create(User domain) { return repository.Save(domain); } public bool Update(User domain) { return repository.Update(domain); } public bool Delete(int id) { return repository.Delete(id); } public List<User> GetAll() { return repository.GetAll(); } } }

Le projet Repositories

Nous arrivons à la dernière section de ce didacticiel : nous avons juste besoin de faire des appels à la base de données, nous créons donc un fichier UserRepository.cs où nous pouvons lire, insérer ou mettre à jour les utilisateurs dans la base de données.

 namespace SeedAPI.Repositories { public class UserRepository : BaseRepository, IUserRepository { public UserRepository(IApplicationContext context) : base(context) { } public User Save(User domain) { try { var us = InsertUser<User>(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser<User>(domain); return true; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Delete(int id) { try { User user = Context.UsersDB.Where(x => x.Id.Equals(id)).FirstOrDefault(); if (user != null) { //Delete<User>(user); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List<User> GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } }

Sommaire

Dans cet article, j'ai expliqué comment créer une bonne architecture en utilisant Angular 5 et Web API Core 2. À ce stade, vous avez créé la base d'un gros projet avec un code qui prend en charge une forte croissance des exigences.

La vérité est que rien ne rivalise avec JavaScript dans le front-end et qu'est-ce qui peut rivaliser avec C# si vous avez besoin du support de SQL Server et Entity Framework dans le back-end ? L'idée de cet article était donc de combiner le meilleur des deux mondes et j'espère que vous l'avez apprécié.

Et après?

Si vous travaillez dans une équipe de développeurs Angular, il pourrait probablement y avoir différents développeurs travaillant dans le front-end et le back-end, donc une bonne idée de synchroniser les efforts des deux équipes pourrait être d'intégrer Swagger avec Web API 2. Swagger est un excellent outil pour documenter et tester vos API RESTFul. Lisez le guide Microsoft : Premiers pas avec Swashbuckle et ASP.NET Core.

Si vous êtes encore très nouveau sur Angular 5 et que vous avez du mal à suivre, lisez Un didacticiel Angular 5: Guide étape par étape de votre première application Angular 5 par son collègue Toptaler Sergey Moiseev.