Angular 5 e ASP.NET Core
Pubblicato: 2022-03-11Ho pensato di scrivere un post sul blog da quando la prima versione di Angular ha praticamente ucciso Microsoft sul lato client. Tecnologie come ASP.Net, Web Forms e MVC Razor sono diventate obsolete, sostituite da un framework JavaScript che non è esattamente Microsoft. Tuttavia, dalla seconda versione di Angular, Microsoft e Google hanno lavorato insieme per creare Angular 2, ed è allora che le mie due tecnologie preferite hanno iniziato a lavorare insieme.
In questo blog, voglio aiutare le persone a creare la migliore architettura combinando questi due mondi. Siete pronti? Eccoci qui!
Circa l'architettura
Creerai un client Angular 5 che utilizza un servizio RESTful Web API Core 2.
Il lato cliente:
- angolare 5
- CLI angolare
- Materiale angolare
Lato server:
- .NET C# API Web Core 2
- Dipendenze di iniezione
- Autenticazione JWT
- Prima il codice del framework dell'entità
- server SQL
Nota
In questo post del blog presumiamo che il lettore abbia già una conoscenza di base di TypeScript, moduli angolari, componenti e importazione/esportazione. L'obiettivo di questo post è creare una buona architettura che consenta al codice di crescere nel tempo. |
Di che cosa hai bisogno?
Iniziamo scegliendo l'IDE. Ovviamente, questa è solo la mia preferenza e puoi usare quella con cui ti senti più a tuo agio. Nel mio caso, utilizzerò Visual Studio Code e Visual Studio 2017.
Perché due IDE diversi? Poiché Microsoft ha creato Visual Studio Code per il front-end, non posso smettere di usare questo IDE. Ad ogni modo, vedremo anche come integrare Angular 5 all'interno del progetto della soluzione, che ti aiuterà se sei il tipo di sviluppatore che preferisce eseguire il debug sia del back-end che del front con un solo F5.
Per quanto riguarda il back-end, puoi installare l'ultima versione di Visual Studio 2017 che ha un'edizione gratuita per gli sviluppatori ma è molto completa: Community.
Quindi, ecco l'elenco delle cose che dobbiamo installare per questo tutorial:
- Codice di Visual Studio
- Comunità di Visual Studio 2017 (o qualsiasi)
- Node.js v8.10.0
- SQL Server 2017
Nota
Verificare di eseguire almeno Node 6.9.xe npm 3.xx eseguendo node -v e npm -v in una finestra del terminale o della console. Le versioni precedenti producono errori, ma le versioni più recenti vanno bene. |
Il Front End
Avvio rapido
Che il divertimento abbia inizio! La prima cosa che dobbiamo fare è installare Angular CLI a livello globale, quindi apri il prompt dei comandi node.js ed esegui questo comando:
npm install -g @angular/cli
Ok, ora abbiamo il nostro bundler di moduli. Questo di solito installa il modulo nella tua cartella utente. Un alias non dovrebbe essere necessario per impostazione predefinita, ma se ne hai bisogno puoi eseguire la riga successiva:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
Il prossimo passo è creare il nuovo progetto. Lo chiamerò angular5-app
. Per prima cosa, andiamo alla cartella in cui vogliamo creare il sito, quindi:
ng new angular5-app
Prima costruzione
Sebbene tu possa testare il tuo nuovo sito Web semplicemente eseguendo ng serve --open
, ti consiglio di testare il sito dal tuo servizio Web preferito. Come mai? Bene, alcuni problemi possono verificarsi solo in produzione e costruire il sito con ng build
è il modo più vicino per avvicinarsi a questo ambiente. Quindi possiamo aprire la cartella angular5-app
con Visual Studio Code ed eseguire ng build
sul terminale bash:
Verrà creata una nuova cartella chiamata dist
e possiamo servirla utilizzando IIS o qualsiasi server web tu preferisca. Quindi puoi digitare l'URL nel browser e... fatto!
Nota
Non è lo scopo di questo tutorial mostrare come configurare un server web, quindi presumo che tu abbia già quella conoscenza. |
La cartella src
La mia cartella src
è strutturata come segue: All'interno della cartella app
abbiamo components
in cui creeremo per ogni componente Angular i file css
, ts
, spec
e html
. Creeremo anche una cartella di config
per mantenere la configurazione del sito, le directives
conterranno tutte le nostre direttive personalizzate, gli helpers
conterranno codice comune come il gestore dell'autenticazione, il layout
conterrà i componenti principali come il corpo, la testa e i pannelli laterali, models
conservano ciò che corrispondono ai modelli di visualizzazione back-end e infine services
avranno il codice per tutte le chiamate al back-end.
Al di fuori della cartella app
manterremo le cartelle create per impostazione predefinita, come assets
e environments
, e anche i file radice.
Creazione del file di configurazione
Creiamo un file config.ts
all'interno della nostra cartella config
e chiamiamo la classe AppConfig
. Qui è dove possiamo impostare tutti i valori che useremo in diversi punti del nostro codice; ad esempio, l'URL dell'API. Si noti che la classe implementa una proprietà get
che riceve, come parametro, una struttura chiave/valore e un metodo semplice per ottenere l'accesso allo stesso valore. In questo modo, sarà facile ottenere i valori semplicemente chiamando this.config.setting['PathAPI']
dalle classi che ereditano da esso.
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]; } };
Materiale angolare
Prima di iniziare il layout, impostiamo il framework del componente UI. Ovviamente puoi usarne altri come Bootstrap, ma se ti piace lo stile di Material, lo consiglio perché è supportato anche da Google.
Per installarlo, dobbiamo solo eseguire i prossimi tre comandi, che possiamo eseguire sul terminale di Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
Il secondo comando è perché alcuni componenti Materiale dipendono dalle animazioni angolari. Consiglio anche di leggere la pagina ufficiale per capire quali browser sono supportati e cos'è un polyfill.
Il terzo comando è perché alcuni componenti Material si basano su HammerJS per i gesti.
Ora possiamo procedere all'importazione dei moduli componenti che vogliamo utilizzare nel nostro file 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 ],
Il prossimo passo è cambiare il file style.css
, aggiungendo il tipo di tema che vuoi usare:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Ora importa HammerJS aggiungendo questa riga nel file main.ts
:
import 'hammerjs';
E infine tutto ciò che ci manca è aggiungere le Icone del materiale a index.html
, all'interno della sezione head:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Lo schema
In questo esempio creeremo un layout semplice come questo:
L'idea è di aprire/nascondere il menu facendo clic su un pulsante nell'intestazione. Angular Responsive farà il resto del lavoro per noi. Per fare ciò creeremo una cartella di layout
e inseriremo al suo interno i file app.component
creati di default. Ma creeremo anche gli stessi file per ogni sezione del layout come puoi vedere nell'immagine successiva. Quindi, app.component
sarà il corpo, head.component
l'intestazione e left-panel.component
il menu.
Ora cambiamo app.component.html
come segue:
<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>
Fondamentalmente avremo una proprietà di authentication
nel componente che ci permetterà di rimuovere l'intestazione e il menu se l'utente non è loggato e mostrare invece una semplice pagina di accesso.
Il head.component.html
si presenta così:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Solo un pulsante per disconnettere l'utente: torneremo su questo più tardi. Per quanto riguarda left-panel.component.html
, per ora basta modificare l'HTML in:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Abbiamo mantenuto le cose semplici: finora sono bastati due link per navigare attraverso due pagine diverse. (Ci torneremo anche su questo più tardi.)
Ora, ecco come appaiono i file TypeScript del componente head e del lato sinistro:
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'; }
Ma che dire del codice TypeScript per app.component
? Lasceremo un piccolo mistero qui e lo metteremo in pausa per un po', e torneremo su questo dopo aver implementato l'autenticazione.
Instradamento
Ok, ora abbiamo Angular Material che ci aiuta con l'interfaccia utente e un layout semplice per iniziare a costruire le nostre pagine. Ma come possiamo navigare tra le pagine?
Per creare un semplice esempio, creiamo due pagine: "Utente", dove possiamo ottenere un elenco degli utenti esistenti nel database, e "Dashboard", una pagina in cui possiamo mostrare alcune statistiche.
All'interno della cartella app
creeremo un file chiamato app-routing.modules.ts
simile a questo:
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 {}
È così semplice: importando RouterModule
e Routes
da @angular/router
, possiamo mappare i percorsi che vogliamo implementare. Qui stiamo creando quattro percorsi:
-
/dashboard
: la nostra home page -
/login
: la pagina in cui l'utente può autenticarsi -
/logout
: un percorso semplice per disconnettere l'utente -
/users
: la nostra prima pagina in cui vogliamo elencare gli utenti dal back-end
Tieni presente che la dashboard
è la nostra pagina per impostazione predefinita, quindi se l'utente digita l'URL /
, la pagina reindirizzerà automaticamente a questa pagina. Inoltre, dai un'occhiata al parametro canActivate
: qui stiamo creando un riferimento alla classe AuthGuard
, che ci permetterà di verificare se l'utente è loggato. In caso contrario, reindirizza alla pagina di accesso. Nella prossima sezione, ti mostrerò come creare questa classe.
Ora, tutto ciò che dobbiamo fare è creare il menu. Ricordi nella sezione del layout quando abbiamo creato il file left-panel.component.html
per assomigliare a questo?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
È qui che il nostro codice incontra la realtà. Ora possiamo creare il codice e testarlo nell'URL: dovresti essere in grado di navigare dalla pagina Dashboard a Utenti, ma cosa succede se digiti l'URL our.site.url/users
direttamente nel browser?
Tieni presente che questo errore viene visualizzato anche se aggiorni il browser dopo aver già navigato correttamente verso quell'URL tramite il pannello laterale dell'app. Per capire questo errore, mi permetto di fare riferimento ai documenti ufficiali dove è proprio chiaro:
Un'applicazione indirizzata dovrebbe supportare i collegamenti diretti. Un collegamento diretto è un URL che specifica un percorso a un componente all'interno dell'app. Ad esempio,
http://www.mysite.com/users/42
è un collegamento diretto alla pagina dei dettagli dell'eroe che mostra l'eroe con ID: 42.Non ci sono problemi quando l'utente passa a quell'URL da un client in esecuzione. Il router Angular interpreta l'URL e indirizza a quella pagina ed eroe.
Ma facendo clic su un collegamento in un'e-mail, immettendolo nella barra degli indirizzi del browser o semplicemente aggiornando il browser nella pagina dei dettagli dell'eroe, tutte queste azioni sono gestite dal browser stesso, al di fuori dell'applicazione in esecuzione. Il browser fa una richiesta diretta al server per quell'URL, bypassando il router.Un server statico restituisce regolarmente index.html quando riceve una richiesta per
http://www.mysite.com/
. Ma rifiutahttp://www.mysite.com/users/42
e restituisce un errore 404 - Non trovato a meno che non sia configurato per restituire invece index.html.
Per risolvere questo problema è molto semplice, dobbiamo solo creare il file di configurazione del provider di servizi. Dato che sto lavorando con IIS qui, ti mostrerò come farlo in questo ambiente, ma il concetto è simile per Apache o qualsiasi altro server web.
Quindi creiamo un file all'interno della cartella src
chiamato web.config
che assomiglia a questo:
<?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>
Quindi dobbiamo essere sicuri che questa risorsa verrà copiata nella cartella distribuita. Tutto quello che dobbiamo fare è modificare il nostro file delle impostazioni della 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": {} } }
Autenticazione
Ricordi come abbiamo implementato la classe AuthGuard
per impostare la configurazione di routing? Ogni volta che navighiamo su una pagina diversa utilizzeremo questa classe per verificare se l'utente è autenticato con un token. In caso contrario, reindirizzeremo automaticamente alla pagina di accesso. Il file per questo è canActivateAuthGuard.ts
: crealo all'interno della cartella helpers
e fallo assomigliare a questo:
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; } }
Quindi ogni volta che cambiamo pagina verrà chiamato il metodo canActivate
, che verificherà se l'utente è autenticato e, in caso contrario, utilizziamo la nostra istanza Router
per reindirizzare alla pagina di accesso. Ma qual è questo nuovo metodo sulla classe Helper
? Nella cartella helpers
creiamo un file helpers.ts
. Qui dobbiamo gestire localStorage
, dove memorizzeremo il token che otteniamo dal back-end.
Nota
Per quanto riguarda localStorage , puoi anche utilizzare cookie o sessionStorage e la decisione dipenderà dal comportamento che vogliamo implementare. Come suggerisce il nome, sessionStorage è disponibile solo per la durata della sessione del browser e viene eliminato alla chiusura della scheda o della finestra; tuttavia, sopravvive ai ricaricamenti delle pagine. Se i dati che stai archiviando devono essere disponibili su base continuativa, localStorage è preferibile a sessionStorage . I cookie servono principalmente per la lettura lato server, mentre localStorage può essere letto solo lato client. Quindi la domanda è, nella tua app, chi ha bisogno di questi dati: il client o il server? |
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()); } }
Il nostro codice di autenticazione ha senso ora? Torneremo alla classe Subject
più tardi, ma ora torniamo indietro per un minuto alla configurazione del percorso. Dai un'occhiata a questa riga:
{ path: 'logout', component: LogoutComponent},
Questo è il nostro componente per disconnettersi dal sito ed è solo una semplice classe per ripulire localStorage
. Creiamolo nella cartella components/login
con il nome di 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']); } }
Quindi ogni volta che andiamo all'URL /logout
, il localStorage
verrà rimosso e il sito reindirizzerà alla pagina di accesso. Infine, creiamo login.component.ts
questo modo:
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']); }); } }
Come puoi vedere, per il momento abbiamo hardcoded le nostre credenziali qui. Nota che qui stiamo chiamando una classe di servizio; creeremo queste classi di servizi per ottenere l'accesso al nostro back-end nella prossima sezione.
Infine, dobbiamo tornare al file app.component.ts
, il layout del sito. Qui, se l'utente è autenticato, mostrerà le sezioni del menu e dell'intestazione, ma in caso contrario, il layout cambierà per mostrare solo la nostra pagina di accesso.

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(); } }
Ricordi la classe Subject
nella nostra classe di supporto? Questo è un Observable
. I messaggi Observable
forniscono supporto per il passaggio di messaggi tra editori e abbonati nell'applicazione. Ogni volta che il token di autenticazione cambia, la proprietà di authentication
verrà aggiornata. Esaminando il file app.component.html
, probabilmente ora avrà più senso:
<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>
Servizi
A questo punto stiamo navigando su diverse pagine, autenticando il nostro lato client e rendendo un layout molto semplice. Ma come possiamo ottenere dati dal back-end? Consiglio vivamente di eseguire in particolare tutti gli accessi back-end dalle classi di servizio . Il nostro primo servizio sarà all'interno della cartella services
, chiamata 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) ); } }
La prima chiamata al back-end è una chiamata POST all'API del token. L'API del token non ha bisogno della stringa del token nell'intestazione, ma cosa succede se chiamiamo un altro endpoint? Come puoi vedere qui, TokenService
(e le classi di servizio in generale) ereditano dalla classe BaseService
. Diamo un'occhiata a questo:
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); } }
Quindi ogni volta che effettuiamo una chiamata HTTP, implementiamo l'intestazione della richiesta semplicemente usando super.header
. Se il token è in localStorage
, verrà aggiunto all'interno dell'intestazione, ma in caso contrario, imposteremo semplicemente il formato JSON. Un'altra cosa che possiamo vedere qui è cosa succede se l'autenticazione fallisce.
Il componente login chiamerà la classe di servizio e la classe di servizio chiamerà il back-end. Una volta ottenuto il token, la classe helper gestirà il token e ora siamo pronti per ottenere l'elenco degli utenti dal nostro database.
Per ottenere i dati dal database, assicurati innanzitutto di abbinare le classi del modello ai modelli di visualizzazione back-end nella nostra risposta.
In user.ts
:
export class User { id: number; name: string; }
E ora possiamo creare il file 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)); }
Il back-end
Avvio rapido
Benvenuti nel primo passaggio della nostra applicazione Web API Core 2. La prima cosa di cui abbiamo bisogno è creare un'applicazione Web ASP.Net Core, che chiameremo SeedAPI.Web.API
.
Assicurati di scegliere il modello vuoto per un inizio pulito come puoi vedere di seguito:
Questo è tutto, creiamo la soluzione partendo da un'applicazione web vuota. Ora la nostra architettura sarà quella che elenchiamo di seguito, quindi dovremo creare i diversi progetti:
Per fare ciò, per ciascuno è sufficiente fare clic con il pulsante destro del mouse sulla Soluzione e aggiungere un progetto "Libreria di classi (.NET Core)".
L'architettura
Nella sezione precedente abbiamo creato otto progetti, ma a cosa servono? Ecco una semplice descrizione di ciascuno:
-
Web.API
: questo è il nostro progetto di avvio e dove vengono creati gli endpoint. Qui configureremo JWT, dipendenze di iniezione e controller. -
ViewModels
: qui eseguiamo conversioni dal tipo di dati che i controller restituiranno nelle risposte al front-end. È buona norma abbinare queste classi ai modelli front-end. -
Interfaces
: questo sarà utile per implementare le dipendenze di iniezione. Il vantaggio convincente di un linguaggio tipizzato statico è che il compilatore può aiutare a verificare che un contratto su cui si basa il codice sia effettivamente soddisfatto. -
Commons
: Tutti i comportamenti condivisi e il codice di utilità saranno qui. -
Models
: è buona norma non abbinare il database direttamente aiViewModels
rivolti al front-end, quindi lo scopo diModels
è creare classi di database di entità indipendenti dal front-end. Ciò ci consentirà in futuro di modificare il nostro database senza necessariamente avere un impatto sul nostro front-end. Aiuta anche quando vogliamo semplicemente fare un po' di refactoring. -
Maps
: qui è doveViewModels
aModels
e viceversa. Questo passaggio viene chiamato tra controller e servizi. -
Services
: una libreria per archiviare tutta la logica aziendale. -
Repositories
: questo è l'unico posto dove chiamiamo il database.
I riferimenti saranno così:
Autenticazione basata su JWT
In questa sezione vedremo la configurazione di base dell'autenticazione dei token e approfondiremo un po' il tema della sicurezza.
Per iniziare a impostare il token web JSON (JWT) creiamo la classe successiva all'interno della cartella App_Start
chiamata JwtTokenConfig.cs
. Il codice all'interno sarà simile a questo:
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(); }); } } }
I valori dei parametri di validazione dipenderanno dal requisito di ciascun progetto. L'utente e il pubblico validi che possiamo impostare leggendo il file di configurazione appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
Quindi dobbiamo solo chiamarlo dal metodo ConfigureServices
in 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(); }
Ora siamo pronti per creare il nostro primo controller chiamato TokenController.cs
. Il valore che impostiamo in appsettings.json
su "veryVerySecretKey"
dovrebbe corrispondere a quello che utilizziamo per creare il token, ma prima creiamo LoginViewModel
all'interno del nostro progetto ViewModels
:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
E infine il controller:
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; } } }
Il metodo BuildToken
creerà il token con il codice di sicurezza specificato. Il metodo Authenticate
ha solo la convalida dell'utente codificata per il momento, ma alla fine dovremo chiamare il database per convalidarlo.
Il contesto dell'applicazione
La configurazione di Entity Framework è davvero semplice da quando Microsoft ha lanciato la versione Core 2.0, in breve EF Core 2 . Approfondiremo un modello code-first usando identityDbContext
, quindi prima assicurati di aver installato tutte le dipendenze. Puoi utilizzare NuGet per gestirlo:
Utilizzando il progetto Models
possiamo creare qui all'interno della cartella Context
due file, ApplicationContext.cs
e IApplicationContext.cs
. Inoltre, avremo bisogno di una classe EntityBase
.
I file EntityBase
verranno ereditati da ogni modello di entità, ma User.cs
è una classe di identità e l'unica entità che erediterà da IdentityUser
. Di seguito sono elencate entrambe le classi:
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(); } } }
Ora siamo pronti per creare ApplicationContext.cs
, che sarà simile a questo:
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(); } } } }
Siamo davvero vicini, ma prima dovremo creare più classi, questa volta nella cartella App_Start
situata nel progetto Web.API
. La prima classe serve per inizializzare il contesto dell'applicazione e la seconda per creare dati di esempio solo allo scopo di testare durante lo sviluppo.
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(); }
Iniezione di dipendenza
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>(); } } }
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.
Il progetto Maps
Questo passaggio serve solo per mappare ViewModels
da e verso modelli di database. Dobbiamo crearne uno per ogni entità e, seguendo il nostro esempio precedente, il file UserMap.cs
sarà simile al seguente:
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; } } }
Sembra ancora una volta che l'iniezione di dipendenza funzioni nel costruttore della classe, collegando Maps al progetto Services.
Il progetto Services
Non c'è molto da dire qui: il nostro esempio è davvero semplice e non abbiamo logica di business o codice da scrivere qui. Questo progetto si rivelerebbe utile in futuri requisiti avanzati quando è necessario calcolare o eseguire una logica prima o dopo i passaggi del database o del controller. Seguendo l'esempio, la classe apparirà piuttosto spoglia:
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(); } } }
Il progetto Repositories
Stiamo arrivando all'ultima sezione di questo tutorial: dobbiamo solo effettuare chiamate al database, quindi creiamo un file UserRepository.cs
in cui possiamo leggere, inserire o aggiornare gli utenti nel database.
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; } } } }
Sommario
In questo articolo ho spiegato come creare una buona architettura utilizzando Angular 5 e Web API Core 2. A questo punto, hai creato la base per un grande progetto con codice che supporta una grande crescita dei requisiti.
La verità è che nulla compete con JavaScript nel front-end e cosa può competere con C# se hai bisogno del supporto di SQL Server ed Entity Framework nel back-end? Quindi l'idea di questo articolo era di combinare il meglio di due mondi e spero che vi sia piaciuto.
Qual è il prossimo?
Se stai lavorando in un team di sviluppatori Angular, probabilmente potrebbero esserci diversi sviluppatori che lavorano nel front-end e nel back-end, quindi una buona idea per sincronizzare gli sforzi di entrambi i team potrebbe essere l'integrazione di Swagger con l'API Web 2. Swagger è un'ottima strumento per documentare e testare le tue API RESTFul. Leggi la guida Microsoft: Inizia con Swashbuckle e ASP.NET Core.
Se sei ancora molto nuovo in Angular 5 e hai problemi a seguirlo, leggi An Angular 5 Tutorial: Guida passo passo alla tua prima app Angular 5 del collega Toptaler Sergey Moiseev.