Angular 5 și ASP.NET Core
Publicat: 2022-03-11M-am gândit să scriu o postare pe blog de când prima versiune a Angular practic a ucis Microsoft pe partea clientului. Tehnologii precum ASP.Net, Web Forms și MVC Razor au devenit învechite, înlocuite cu un cadru JavaScript care nu este tocmai Microsoft. Cu toate acestea, de la cea de-a doua versiune a Angular, Microsoft și Google au lucrat împreună pentru a crea Angular 2, și acesta este momentul în care cele două tehnologii ale mele preferate au început să lucreze împreună.
În acest blog, vreau să ajut oamenii să creeze cea mai bună arhitectură care combină aceste două lumi. Sunteți gata? Începem!
Despre arhitectura
Veți construi un client Angular 5 care consumă un serviciu RESTful Web API Core 2.
Partea clientului:
- unghiular 5
- CLI unghiular
- Material unghiular
Partea serverului:
- .NET C# Web API Core 2
- Dependențe de injecție
- Autentificare JWT
- Mai întâi codul cadru al entității
- SQL Server
Notă
În această postare pe blog presupunem că cititorul are deja cunoștințe de bază despre TypeScript, module Angular, componente și import/export. Scopul acestei postări este de a crea o arhitectură bună care să permită ca codul să crească în timp. |
De ce ai nevoie?
Să începem prin a alege IDE-ul. Desigur, aceasta este doar preferința mea și o poți folosi pe cea cu care te simți mai confortabil. În cazul meu, voi folosi Visual Studio Code și Visual Studio 2017.
De ce două IDE-uri diferite? Deoarece Microsoft a creat Visual Studio Code pentru front-end, nu pot să nu mai folosesc acest IDE. Oricum, vom vedea, de asemenea, cum să integrăm Angular 5 în proiectul de soluție, asta vă va ajuta dacă sunteți genul de dezvoltator care preferă să depaneze atât back end, cât și front cu un singur F5.
Despre back-end, puteți instala cea mai recentă versiune Visual Studio 2017 care are o ediție gratuită pentru dezvoltatori, dar este foarte completă: Comunitate.
Deci, iată lista de lucruri pe care trebuie să le instalăm pentru acest tutorial:
- Codul Visual Studio
- Comunitatea Visual Studio 2017 (sau oricare)
- Node.js v8.10.0
- SQL Server 2017
Notă
Verificați dacă rulați cel puțin Node 6.9.x și npm 3.xx rulând node -v și npm -v într-o fereastră de terminal sau consolă. Versiunile mai vechi produc erori, dar versiunile mai noi sunt bine. |
Front End
Pornire rapidă
Sa inceapa distractia! Primul lucru pe care trebuie să-l facem este să instalăm Angular CLI la nivel global, așa că deschideți promptul de comandă node.js și rulați această comandă:
npm install -g @angular/cli
Bine, acum avem pachetul nostru de module. Acest lucru instalează de obicei modulul în folderul dvs. de utilizator. Un alias nu ar trebui să fie necesar în mod implicit, dar dacă aveți nevoie de el puteți executa următoarea linie:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
Următorul pas este crearea noului proiect. O voi numi angular5-app
. Mai întâi, navigăm la folderul în care dorim să creăm site-ul și apoi:
ng new angular5-app
Prima construcție
Deși vă puteți testa noul site web doar rulând ng serve --open
, vă recomand să testați site-ul de la serviciul dvs. web preferat. De ce? Ei bine, unele probleme se pot întâmpla numai în producție, iar construirea site-ului cu ng build
este cea mai apropiată modalitate de a aborda acest mediu. Apoi putem deschide folderul angular5-app
cu Visual Studio Code și rulăm ng build
pe terminalul bash:
Va fi creat un nou folder numit dist
și îl putem servi folosind IIS sau orice server web doriți. Apoi puteți introduce adresa URL în browser și... gata!
Notă
Scopul acestui tutorial nu este de a arăta cum să configurați un server web, așa că presupun că aveți deja aceste cunoștințe. |
Dosarul src
Dosarul meu src
este structurat după cum urmează: În interiorul folderului de app
avem components
în care vom crea pentru fiecare componentă Angular fișierele css
, ts
, spec
și html
. De asemenea, vom crea un folder de config
pentru a păstra configurația site-ului, directives
vor avea toate directivele noastre personalizate, helpers
vor găzdui cod comun, cum ar fi managerul de autentificare, layout
va conține componentele principale precum corpul, capul și panourile laterale, models
păstrează ceea ce va se potrivesc cu modelele de vizualizare back-end, iar în cele din urmă services
vor avea codul pentru toate apelurile către back-end.
În afara folderului app
, vom păstra folderele create implicit, cum ar fi assets
și environments
, precum și fișierele rădăcină.
Crearea fișierului de configurare
Să creăm un fișier config.ts
în folderul nostru de config
și să apelăm clasa AppConfig
. Aici putem seta toate valorile pe care le vom folosi în diferite locuri din codul nostru; de exemplu, adresa URL a API-ului. Rețineți că clasa implementează o proprietate get
care primește, ca parametru, o structură cheie/valoare și o metodă simplă de a obține acces la aceeași valoare. În acest fel, va fi ușor să obțineți valorile doar apelând this.config.setting['PathAPI']
din clasele care moștenesc de la acesta.
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]; } };
Material unghiular
Înainte de a începe aspectul, să setăm cadrul pentru componentele UI. Desigur, puteți folosi altele precum Bootstrap, dar dacă vă place stilul Materialului, îl recomand pentru că este acceptat și de Google.
Pentru a-l instala, trebuie doar să rulăm următoarele trei comenzi, pe care le putem executa pe terminalul Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
A doua comandă se datorează faptului că unele componente ale Materialului depind de Animațiile Angular. De asemenea, vă recomand să citiți pagina oficială pentru a înțelege ce browsere sunt acceptate și ce este un polyfill.
A treia comandă se datorează faptului că unele componente Material se bazează pe HammerJS pentru gesturi.
Acum putem continua să importam modulele componente pe care dorim să le folosim în fișierul nostru 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 ],
Următorul pas este să schimbați fișierul style.css
, adăugând tipul de temă pe care doriți să o utilizați:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Acum importați HammerJS adăugând această linie în fișierul main.ts
:
import 'hammerjs';
Și, în sfârșit, tot ce ne lipsește este să adăugăm pictogramele Material la index.html
, în interiorul secțiunii head:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Schema
În acest exemplu, vom crea un aspect simplu ca acesta:
Ideea este să deschideți/ascundeți meniul făcând clic pe un buton din antet. Angular Responsive va face restul muncii pentru noi. Pentru a face acest lucru vom crea un folder de layout
și vom pune în el fișierele app.component
create implicit. Dar vom crea și aceleași fișiere pentru fiecare secțiune a aspectului, așa cum puteți vedea în imaginea următoare. Apoi, app.component
va fi body, head.component
header și left-panel.component
meniul.
Acum să schimbăm app.component.html
după cum urmează:
<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>
Practic vom avea o proprietate de authentication
în componentă care ne va permite să eliminăm antetul și meniul dacă utilizatorul nu este autentificat și, în schimb, să arătăm o pagină simplă de autentificare.
head.component.html
arată astfel:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Doar un buton pentru a deconecta utilizatorul - vom reveni la asta mai târziu. În ceea ce privește left-panel.component.html
, deocamdată doar schimbați HTML-ul în:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Am păstrat totul simplu: până acum sunt doar două link-uri pentru a naviga prin două pagini diferite. (Vom reveni și la asta mai târziu.)
Acum, așa arată fișierele TypeScript cap și componente din partea stângă:
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'; }
Dar ce zici de codul TypeScript pentru app.component
? Vom lăsa un mic mister aici și îl vom întrerupe pentru un timp și vom reveni la asta după implementarea autentificării.
Dirijare
Bine, acum avem Angular Material care ne ajută cu interfața de utilizare și un aspect simplu pentru a începe să construim paginile noastre. Dar cum putem naviga între pagini?
Pentru a crea un exemplu simplu, să creăm două pagini: „Utilizator”, de unde putem obține o listă cu utilizatorii existenți în baza de date și „Tabloul de bord”, o pagină în care putem afișa câteva statistici.
În dosarul app
, vom crea un fișier numit app-routing.modules.ts
arătând astfel:
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 {}
Este atât de simplu: doar importând RouterModule
și Routes
din @angular/router
, putem mapa căile pe care vrem să le implementăm. Aici creăm patru căi:
-
/dashboard
: Pagina noastră de pornire -
/login
: Pagina în care utilizatorul se poate autentifica -
/logout
: O cale simplă pentru deconectarea utilizatorului -
/users
: Prima noastră pagină în care dorim să listăm utilizatorii din back-end
Rețineți că dashboard
de bord este pagina noastră în mod implicit, așa că dacă utilizatorul introduce adresa URL /
, pagina va fi redirecționată automat către această pagină. De asemenea, aruncați o privire la parametrul canActivate
: Aici creăm o referință la clasa AuthGuard
, care ne va permite să verificăm dacă utilizatorul este autentificat. Dacă nu, redirecționează către pagina de autentificare. În secțiunea următoare, vă voi arăta cum să creați această clasă.
Acum, tot ce trebuie să facem este să creăm meniul. Vă amintiți în secțiunea de aspect când am creat fișierul left-panel.component.html
să arate așa?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Aici codul nostru se întâlnește cu realitatea. Acum putem construi codul și îl putem testa în URL: ar trebui să puteți naviga din pagina Tabloul de bord la Utilizatori, dar ce se întâmplă dacă introduceți adresa URL our.site.url/users
direct în browser?
Rețineți că această eroare apare și dacă reîmprospătați browserul după ce ați navigat deja cu succes la acea adresă URL prin panoul lateral al aplicației. Pentru a înțelege această eroare, permiteți-mi să mă refer la documentele oficiale unde este foarte clar:
O aplicație direcționată ar trebui să accepte legături profunde. Un link profund este o adresă URL care specifică o cale către o componentă din interiorul aplicației. De exemplu,
http://www.mysite.com/users/42
este un link profund către pagina cu detalii despre erou care afișează eroul cu id: 42.Nu există nicio problemă când utilizatorul navighează la acea adresă URL dintr-un client care rulează. Routerul Angular interpretează adresa URL și rutele către pagina respectivă și eroul.
Dar făcând clic pe un link dintr-un e-mail, introducerea acestuia în bara de adrese a browserului sau pur și simplu reîmprospătarea browserului în timp ce se află pe pagina de detalii eroului - toate aceste acțiuni sunt gestionate de browser însuși, în afara aplicației care rulează. Browserul face o cerere directă către server pentru acea adresă URL, ocolind routerul.Un server static returnează în mod obișnuit index.html când primește o solicitare pentru
http://www.mysite.com/
. Dar respingehttp://www.mysite.com/users/42
și returnează o eroare 404 - Not Found, cu excepția cazului în care este configurat să returneze index.html.
Pentru a remedia această problemă este foarte simplu, trebuie doar să creăm configurația fișierului furnizorului de servicii. Deoarece lucrez cu IIS aici, vă voi arăta cum să o faceți în acest mediu, dar conceptul este similar pentru Apache sau orice alt server web.
Așadar, creăm un fișier în interiorul folderului src
numit web.config
care arată astfel:
<?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>
Apoi trebuie să ne asigurăm că acest activ va fi copiat în folderul implementat. Tot ce trebuie să facem este să ne schimbăm fișierul de setări Angular 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": {} } }
Autentificare
Vă amintiți cum am implementat clasa AuthGuard
pentru a seta configurația de rutare? De fiecare dată când navigăm la o pagină diferită, vom folosi această clasă pentru a verifica dacă utilizatorul este autentificat cu un token. Dacă nu, vom redirecționa automat către pagina de conectare. Fișierul pentru aceasta este canActivateAuthGuard.ts
- creați-l în folderul helpers
și lăsați-l să arate astfel:
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; } }
Deci, de fiecare dată când schimbăm pagina, va fi apelată metoda canActivate
, care va verifica dacă utilizatorul este autentificat, iar dacă nu, folosim instanța noastră Router
pentru a redirecționa către pagina de autentificare. Dar ce este această nouă metodă la clasa Helper
? În folderul helpers
, să creăm un fișier helpers.ts
. Aici trebuie să gestionăm localStorage
, unde vom stoca jetonul pe care îl primim de la back-end.
Notă
În ceea ce privește localStorage , puteți utiliza și cookie-uri sau sessionStorage , iar decizia va depinde de comportamentul pe care vrem să îl implementăm. După cum sugerează și numele, sessionStorage este disponibil numai pe durata sesiunii de browser și este șters atunci când fila sau fereastra este închisă; totuși, supraviețuiește reîncărcărilor paginilor. Dacă datele pe care le stocați trebuie să fie disponibile în mod continuu, atunci localStorage este de preferat în sessionStorage . Cookie-urile sunt în principal pentru citirea pe partea serverului, în timp ce localStorage poate fi citit doar pe partea client. Deci întrebarea este, în aplicația dvs., cine are nevoie de aceste date --- clientul sau serverul? |
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()); } }
Codul nostru de autentificare are sens acum? Vom reveni la clasa Subject
mai târziu, dar acum să ne întoarcem un minut la configurația de rutare. Aruncă o privire la această linie:
{ path: 'logout', component: LogoutComponent},
Aceasta este componenta noastră pentru a vă deconecta de pe site și este doar o clasă simplă pentru a curăța localStorage
. Să-l creăm în folderul components/login
cu numele 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']); } }
Deci, de fiecare dată când mergem la adresa URL /logout
, localStorage
va fi eliminat și site-ul va redirecționa către pagina de conectare. În cele din urmă, să creăm login.component.ts
astfel:
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']); }); } }
După cum puteți vedea, pentru moment ne-am codificat acreditările aici. Rețineți că aici numim o clasă de servicii; vom crea aceste clase de servicii pentru a avea acces la back-end-ul nostru în secțiunea următoare.

În cele din urmă, trebuie să revenim la fișierul app.component.ts
, aspectul site-ului. Aici, dacă utilizatorul este autentificat, va afișa meniul și secțiunile antetului, dar dacă nu, aspectul se va schimba pentru a afișa doar pagina noastră de conectare.
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(); } }
Vă amintiți clasa Subject
din clasa noastră de ajutor? Acesta este un Observable
. Observable
oferă suport pentru transmiterea de mesaje între editori și abonați în aplicația dvs. De fiecare dată când simbolul de autentificare se modifică, proprietatea de authentication
va fi actualizată. Examinând fișierul app.component.html
, probabil că va avea mai mult sens acum:
<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>
Servicii
În acest moment, navigăm la diferite pagini, ne autentificăm partea client și redăm un aspect foarte simplu. Dar cum putem obține date din back-end? Recomand cu tărie să faceți toate accesul back-end din clasele de servicii în special. Primul nostru serviciu va fi în folderul de services
, numit 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) ); } }
Primul apel către back-end este un apel POST către API-ul token. API-ul token nu are nevoie de șirul de token din antet, dar ce se întâmplă dacă numim un alt punct final? După cum puteți vedea aici, TokenService
(și clasele de servicii în general) moștenesc din clasa BaseService
. Să aruncăm o privire la asta:
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); } }
Deci, de fiecare dată când facem un apel HTTP, implementăm antetul cererii doar folosind super.header
. Dacă jetonul este în localStorage
, atunci va fi adăugat în antet, dar dacă nu, vom seta doar formatul JSON. Un alt lucru pe care îl putem vedea aici este ce se întâmplă dacă autentificarea eșuează.
Componenta de autentificare va apela clasa de servicii, iar clasa de servicii va apela back end. Odată ce avem jetonul, clasa helper va gestiona jetonul, iar acum suntem gata să obținem lista de utilizatori din baza noastră de date.
Pentru a obține date din baza de date, asigurați-vă mai întâi că potrivim clasele de model cu modelele de vizualizare back-end din răspunsul nostru.
În user.ts
:
export class User { id: number; name: string; }
Și putem crea acum fișierul 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)); }
The Back End
Pornire rapidă
Bun venit la primul pas al aplicației noastre Web API Core 2. Primul lucru de care avem nevoie este să creăm o aplicație web ASP.Net Core, pe care o vom numi SeedAPI.Web.API
.
Asigurați-vă că alegeți șablonul Gol pentru un început curat, așa cum puteți vedea mai jos:
Asta e tot, creăm soluția începând cu o aplicație web goală. Acum arhitectura noastră va fi așa cum enumeram mai jos, așa că va trebui să creăm diferite proiecte:
Pentru a face acest lucru, pentru fiecare, faceți clic dreapta pe Soluție și adăugați un proiect „Class Library (.NET Core)”.
Arhitectura
În secțiunea anterioară am creat opt proiecte, dar pentru ce sunt acestea? Iată o descriere simplă a fiecăruia:
-
Web.API
: Acesta este proiectul nostru de pornire și unde sunt create punctele finale. Aici vom configura JWT, dependențele de injecție și controlere. -
ViewModels
: Aici efectuăm conversii din tipul de date pe care controlorii le vor returna în răspunsurile către front-end. Este o practică bună să potriviți aceste clase cu modelele front-end. -
Interfaces
: Acest lucru va fi util în implementarea dependențelor de injecție. Avantajul convingător al unui limbaj tip static este că compilatorul poate ajuta la verificarea faptului că un contract pe care se bazează codul dumneavoastră este într-adevăr îndeplinit. -
Commons
: Toate comportamentele partajate și codul de utilitate vor fi aici. -
Models
: Este o practică bună să nu potriviți baza de date direct cuViewModels
orientate către front-end, deci scopulModels
este de a crea clase de baze de date de entități independente de front-end. Acest lucru ne va permite în viitor să ne schimbăm baza de date fără a avea neapărat un impact asupra front-end-ului nostru. De asemenea, ajută atunci când vrem pur și simplu să facem niște refactorizări. -
Maps
: AiciViewModels
laModels
și viceversa. Acest pas este numit între controlori și Servicii. -
Services
: O bibliotecă pentru a stoca toată logica de afaceri. -
Repositories
: Acesta este singurul loc în care numim baza de date.
Referințele vor arăta astfel:
Autentificare bazată pe JWT
În această secțiune, vom vedea configurația de bază a autentificării cu token și vom aprofunda puțin subiectul securității.
Pentru a începe setarea jetonului web JSON (JWT), să creăm următoarea clasă în folderul App_Start
numit JwtTokenConfig.cs
. Codul din interior va arăta astfel:
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(); }); } } }
Valorile parametrilor de validare vor depinde de cerințele fiecărui proiect. Utilizatorul și publicul valid pe care îl putem seta citind fișierul de configurare appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
Apoi trebuie doar să-l apelăm din metoda ConfigureServices
din 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(); }
Acum suntem gata să creăm primul nostru controler numit TokenController.cs
. Valoarea pe care o setăm în appsettings.json
la "veryVerySecretKey"
ar trebui să se potrivească cu cea pe care o folosim pentru a crea simbolul, dar mai întâi, să creăm LoginViewModel
în cadrul proiectului nostru ViewModels
:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
Și în sfârșit controlerul:
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; } } }
Metoda BuildToken
va crea jetonul cu codul de securitate dat. Metoda Authenticate
are doar validarea utilizatorului codificată pentru moment, dar va trebui să apelăm baza de date pentru a o valida în final.
Contextul aplicației
Configurarea Entity Framework este foarte ușoară, deoarece Microsoft a lansat versiunea Core 2.0 – EF Core 2 pe scurt. Vom intra în profunzime cu un model de cod mai întâi folosind identityDbContext
, așa că mai întâi asigurați-vă că ați instalat toate dependențele. Puteți folosi NuGet pentru a-l gestiona:
Folosind proiectul Models
putem crea aici în dosarul Context
două fișiere, ApplicationContext.cs
și IApplicationContext.cs
. De asemenea, vom avea nevoie de o clasă EntityBase
.
Fișierele EntityBase
vor fi moștenite de fiecare model de entitate, dar User.cs
este o clasă de identitate și singura entitate care va moșteni de la IdentityUser
. Mai jos sunt ambele clase:
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(); } } }
Acum suntem gata să creăm ApplicationContext.cs
, care va arăta astfel:
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(); } } } }
Suntem foarte aproape, dar mai întâi va trebui să creăm mai multe clase, de data aceasta în folderul App_Start
aflat în proiectul Web.API
. Prima clasă este de a inițializa contextul aplicației, iar a doua este de a crea date eșantioane doar în scopul testării în timpul dezvoltării.
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(); }
Injecție de dependență
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.
Proiectul Maps
Acest pas este doar pentru a mapa ViewModels
către și de la modele de baze de date. Trebuie să creăm câte unul pentru fiecare entitate și, urmând exemplul nostru anterior, fișierul UserMap.cs
va arăta astfel:
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; } } }
Se pare că încă o dată, injecția de dependență funcționează în constructorul clasei, legând Maps la proiectul Servicii.
Proiectul Services
Nu sunt prea multe de spus aici: exemplul nostru este foarte simplu și nu avem logică de afaceri sau cod de scris aici. Acest proiect s-ar dovedi util în cerințele avansate viitoare când trebuie să calculăm sau să facem ceva logică înainte sau după pașii bazei de date sau controlerului. Urmând exemplul, clasa va arăta destul de goală:
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(); } } }
Proiectul Repositories
Ajungem la ultima secțiune a acestui tutorial: trebuie doar să facem apeluri la baza de date, așa că creăm un fișier UserRepository.cs
unde putem citi, insera sau actualiza utilizatorii din baza de date.
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; } } } }
rezumat
În acest articol, am explicat cum să creați o arhitectură bună folosind Angular 5 și Web API Core 2. În acest moment, ați creat baza pentru un proiect mare cu cod care susține o creștere mare a cerințelor.
Adevărul este că nimic nu concurează cu JavaScript în front-end și ce poate concura cu C# dacă aveți nevoie de suport pentru SQL Server și Entity Framework în back-end? Așa că ideea acestui articol a fost să îmbine cele mai bune dintre două lumi și sper că v-a plăcut.
Ce urmeaza?
Dacă lucrați într-o echipă de dezvoltatori Angular, probabil că ar putea exista diferiți dezvoltatori care lucrează în front-end și back-end, așa că o idee bună de a sincroniza eforturile ambelor echipe ar putea fi integrarea Swagger cu Web API 2. Swagger este un excelent instrument pentru a vă documenta și testa API-urile RESTFul. Citiți ghidul Microsoft: Începeți cu Swashbuckle și ASP.NET Core.
Dacă sunteți încă foarte nou în Angular 5 și întâmpinați dificultăți de urmărire, citiți Un tutorial Angular 5: Ghid pas cu pas pentru prima dvs. aplicație Angular 5 de la colegul Toptaler Sergey Moiseev.