Angular 5 und ASP.NET Core
Veröffentlicht: 2022-03-11Ich habe darüber nachgedacht, einen Blogbeitrag zu schreiben, seit die erste Version von Angular Microsoft auf der Client-Seite praktisch getötet hat. Technologien wie ASP.Net, Web Forms und MVC Razor sind veraltet und wurden durch ein JavaScript-Framework ersetzt, das nicht gerade von Microsoft stammt. Seit der zweiten Version von Angular arbeiten Microsoft und Google jedoch zusammen, um Angular 2 zu entwickeln, und zu diesem Zeitpunkt begannen meine beiden Lieblingstechnologien zusammenzuarbeiten.
In diesem Blog möchte ich Menschen helfen, die beste Architektur zu schaffen, die diese beiden Welten kombiniert. Sind Sie bereit? Auf geht's!
Über die Architektur
Sie erstellen einen Angular 5-Client, der einen RESTful Web API Core 2-Dienst nutzt.
Die Kundenseite:
- Winkel 5
- Winkel-CLI
- Kantiges Material
Die Serverseite:
- .NET C#-Web-API Core 2
- Injektionsabhängigkeiten
- JWT-Authentifizierung
- Entity-Framework-Code zuerst
- SQL Server
Notiz
In diesem Blogbeitrag gehen wir davon aus, dass der Leser bereits über Grundkenntnisse in TypeScript, Angular-Modulen, Komponenten und Import/Export verfügt. Das Ziel dieses Beitrags ist es, eine gute Architektur zu erstellen, die es ermöglicht, dass der Code im Laufe der Zeit wächst. |
Was brauchen Sie?
Beginnen wir mit der Auswahl der IDE. Dies ist natürlich nur meine Präferenz, und Sie können die verwenden, mit der Sie sich wohler fühlen. In meinem Fall verwende ich Visual Studio Code und Visual Studio 2017.
Warum zwei verschiedene IDEs? Da Microsoft Visual Studio Code für das Frontend erstellt hat, kann ich nicht aufhören, diese IDE zu verwenden. Wie auch immer, wir werden auch sehen, wie man Angular 5 in das Lösungsprojekt integriert, das wird Ihnen helfen, wenn Sie die Art von Entwickler sind, die es vorzieht, sowohl Backend als auch Front mit nur einem F5 zu debuggen.
Über das Backend können Sie die neueste Version von Visual Studio 2017 installieren, die eine kostenlose Edition für Entwickler hat, aber sehr vollständig ist: Community.
Hier also die Liste der Dinge, die wir für dieses Tutorial installieren müssen:
- Visual Studio-Code
- Visual Studio 2017-Community (oder alle)
- Node.js v8.10.0
- SQL-Server 2017
Notiz
Stellen Sie sicher, dass Sie mindestens Node 6.9.x und npm 3.xx ausführen, indem Sie node -v und npm -v in einem Terminal- oder Konsolenfenster ausführen. Ältere Versionen erzeugen Fehler, aber neuere Versionen sind in Ordnung. |
Das Frontend
Schnellstart
Lass den Spaß beginnen! Als erstes müssen wir Angular CLI global installieren. Öffnen Sie also die Eingabeaufforderung von node.js und führen Sie diesen Befehl aus:
npm install -g @angular/cli
Okay, jetzt haben wir unseren Modulbundler. Dadurch wird das Modul normalerweise in Ihrem Benutzerordner installiert. Ein Alias sollte standardmäßig nicht notwendig sein, aber wenn Sie ihn brauchen, können Sie die nächste Zeile ausführen:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
Im nächsten Schritt erstellen Sie das neue Projekt. Ich werde es angular5-app
nennen. Zuerst navigieren wir zu dem Ordner, unter dem wir die Site erstellen möchten, und dann:
ng new angular5-app
Erster Aufbau
Während Sie Ihre neue Website testen können, indem Sie einfach ng serve --open
, empfehle ich, die Website von Ihrem bevorzugten Webdienst aus zu testen. Warum? Nun, einige Probleme können nur in der Produktion auftreten, und das Erstellen der Site mit ng build
ist der nächste Weg, um sich dieser Umgebung zu nähern. Dann können wir den Ordner angular5-app
mit Visual Studio Code öffnen und ng build
auf der Terminal-Bash ausführen:
Ein neuer Ordner namens dist
wird erstellt und wir können ihn mit IIS oder einem anderen von Ihnen bevorzugten Webserver bereitstellen. Dann können Sie die URL in den Browser eingeben und … fertig!
Notiz
Es ist nicht der Zweck dieses Tutorials zu zeigen, wie man einen Webserver einrichtet, also gehe ich davon aus, dass Sie dieses Wissen bereits haben. |
Der src
Ordner
Mein src
Ordner ist wie folgt strukturiert: Innerhalb des app
-Ordners haben wir components
, in denen wir für jede Angular-Komponente die css
-, ts
-, spec
- und html
-Dateien erstellen werden. Wir werden auch einen config
erstellen, um die Site-Konfiguration beizubehalten, directives
werden alle unsere benutzerdefinierten Direktiven enthalten, helpers
werden allgemeinen Code wie den Authentifizierungsmanager enthalten, das layout
wird die Hauptkomponenten wie Körper, Kopf und Seitenteile enthalten, models
behalten, was will mit den Back-End-Ansichtsmodellen übereinstimmen, und schließlich haben die services
den Code für alle Aufrufe an das Back-End.
Außerhalb des app
-Ordners behalten wir die standardmäßig erstellten Ordner wie assets
und environments
sowie die Root-Dateien.
Erstellen der Konfigurationsdatei
Lassen Sie uns eine config.ts
-Datei in unserem config
erstellen und die Klasse AppConfig
. Hier können wir alle Werte festlegen, die wir an verschiedenen Stellen in unserem Code verwenden werden. beispielsweise die URL der API. Beachten Sie, dass die Klasse eine get
-Eigenschaft implementiert, die als Parameter eine Schlüssel/Wert-Struktur und eine einfache Methode erhält, um Zugriff auf denselben Wert zu erhalten. Auf diese Weise ist es einfach, die Werte abzurufen, indem Sie einfach this.config.setting['PathAPI']
von den Klassen aufrufen, die davon erben.
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]; } };
Kantiges Material
Bevor wir mit dem Layout beginnen, richten wir das UI-Komponenten-Framework ein. Natürlich können Sie andere wie Bootstrap verwenden, aber wenn Ihnen das Styling von Material gefällt, empfehle ich es, da es auch von Google unterstützt wird.
Um es zu installieren, müssen wir nur die nächsten drei Befehle ausführen, die wir auf dem Visual Studio Code-Terminal ausführen können:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
Der zweite Befehl liegt daran, dass einige Materialkomponenten von Winkelanimationen abhängen. Ich empfehle auch, die offizielle Seite zu lesen, um zu verstehen, welche Browser unterstützt werden und was ein Polyfill ist.
Der dritte Befehl liegt daran, dass einige Materialkomponenten für Gesten auf HammerJS angewiesen sind.
Jetzt können wir mit dem Importieren der Komponentenmodule fortfahren, die wir in unserer Datei app.module.ts
verwenden möchten:
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 ],
Der nächste Schritt besteht darin, die style.css
-Datei zu ändern und die Art des Themas hinzuzufügen, das Sie verwenden möchten:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Importieren Sie nun HammerJS, indem Sie diese Zeile in die Datei main.ts
:
import 'hammerjs';
Und schließlich fehlt uns nur noch das Hinzufügen der Materialsymbole zu index.html
im Head-Bereich:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Das Layout
In diesem Beispiel erstellen wir ein einfaches Layout wie dieses:
Die Idee ist, das Menü zu öffnen/auszublenden, indem Sie auf eine Schaltfläche in der Kopfzeile klicken. Angular Responsive erledigt den Rest der Arbeit für uns. Dazu erstellen wir einen layout
und legen darin die standardmäßig erstellten app.component
Dateien ab. Aber wir werden auch die gleichen Dateien für jeden Abschnitt des Layouts erstellen, wie Sie im nächsten Bild sehen können. Dann ist app.component
der Körper, head.component
der Header und left-panel.component
das Menü.
Ändern wir nun app.component.html
wie folgt:
<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>
Grundsätzlich haben wir eine authentication
in der Komponente, die es uns ermöglicht, die Kopfzeile und das Menü zu entfernen, wenn der Benutzer nicht angemeldet ist, und stattdessen eine einfache Anmeldeseite anzuzeigen.
Die head.component.html
sieht so aus:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Nur eine Schaltfläche zum Abmelden des Benutzers – wir kommen später noch einmal darauf zurück. Ändern Sie für left-panel.component.html
einfach den HTML-Code in:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Wir haben es einfach gehalten: Bisher sind es nur zwei Links, um durch zwei verschiedene Seiten zu navigieren. (Auch hierauf kommen wir später zurück.)
So sehen nun die TypeScript-Dateien der Head- und der linken Komponente aus:
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'; }
Aber was ist mit dem TypeScript-Code für app.component
? Wir werden hier ein kleines Rätsel hinterlassen und es für eine Weile pausieren und nach der Implementierung der Authentifizierung darauf zurückkommen.
Routing
Okay, jetzt haben wir Angular Material, das uns bei der Benutzeroberfläche und einem einfachen Layout hilft, um mit dem Erstellen unserer Seiten zu beginnen. Aber wie können wir zwischen den Seiten navigieren?
Um ein einfaches Beispiel zu erstellen, erstellen wir zwei Seiten: „Benutzer“, auf der wir eine Liste der vorhandenen Benutzer in der Datenbank abrufen können, und „Dashboard“, eine Seite, auf der wir einige Statistiken anzeigen können.
Im app
-Ordner erstellen wir eine Datei namens app-routing.modules.ts
, die so aussieht:
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 {}
So einfach ist das: Durch einfaches Importieren von RouterModule
und Routes
aus @angular/router
können wir die Pfade abbilden, die wir implementieren möchten. Hier erstellen wir vier Pfade:
-
/dashboard
: Unsere Homepage -
/login
: Die Seite, auf der sich der Benutzer authentifizieren kann -
/logout
: Ein einfacher Pfad zum Abmelden des Benutzers -
/users
: Unsere erste Seite, auf der wir die Benutzer aus dem Backend auflisten möchten
Beachten Sie, dass das dashboard
standardmäßig unsere Seite ist. Wenn der Benutzer also die URL /
eingibt, wird die Seite automatisch auf diese Seite umgeleitet. Sehen Sie sich auch den Parameter canActivate
an: Hier erstellen wir eine Referenz auf die Klasse AuthGuard
, mit der wir überprüfen können, ob der Benutzer angemeldet ist. Wenn nicht, wird auf die Anmeldeseite umgeleitet. Im nächsten Abschnitt zeige ich Ihnen, wie Sie diese Klasse erstellen.
Jetzt müssen wir nur noch das Menü erstellen. Erinnern Sie sich an den Abschnitt Layout, als wir die Datei left-panel.component.html
erstellt haben, um so auszusehen?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Hier trifft unser Code auf die Realität. Jetzt können wir den Code bauen und in der URL testen: Sie sollten in der Lage sein, von der Dashboard-Seite zu Benutzern zu navigieren, aber was passiert, wenn Sie die URL our.site.url/users
direkt in den Browser eingeben?
Beachten Sie, dass dieser Fehler auch auftritt, wenn Sie den Browser aktualisieren, nachdem Sie bereits erfolgreich zu dieser URL über die Seitenleiste der App navigiert haben. Um diesen Fehler zu verstehen, erlauben Sie mir, auf die offiziellen Dokumente zu verweisen, wo es wirklich klar ist:
Eine geroutete Anwendung sollte Deep Links unterstützen. Ein Deep-Link ist eine URL, die einen Pfad zu einer Komponente innerhalb der App angibt. Beispielsweise ist
http://www.mysite.com/users/42
ein Deep-Link zur Helden-Detailseite, die den Helden mit der ID: 42 anzeigt.Es gibt kein Problem, wenn der Benutzer von einem laufenden Client aus zu dieser URL navigiert. Der Angular-Router interpretiert die URL und leitet zu dieser Seite und diesem Helden weiter.
Aber das Klicken auf einen Link in einer E-Mail, das Eingeben in die Adressleiste des Browsers oder das Aktualisieren des Browsers auf der Hero-Detailseite – all diese Aktionen werden vom Browser selbst ausgeführt, außerhalb der laufenden Anwendung. Der Browser fordert diese URL direkt beim Server an, wobei der Router umgangen wird.Ein statischer Server gibt routinemäßig index.html zurück, wenn er eine Anfrage für
http://www.mysite.com/
erhält. Aber es lehnthttp://www.mysite.com/users/42
ab und gibt einen 404 - Not Found-Fehler zurück, es sei denn, es ist so konfiguriert, dass es stattdessen index.html zurückgibt.
Um dieses Problem zu beheben, ist es sehr einfach, wir müssen nur die Dateikonfiguration des Dienstanbieters erstellen. Da ich hier mit IIS arbeite, werde ich Ihnen zeigen, wie es in dieser Umgebung geht, aber das Konzept ist für Apache oder jeden anderen Webserver ähnlich.
Also erstellen wir eine Datei im src
-Ordner namens web.config
, die so aussieht:
<?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>
Dann müssen wir sicherstellen, dass dieses Asset in den bereitgestellten Ordner kopiert wird. Alles, was wir tun müssen, ist unsere Angular-CLI-Einstellungsdatei angular-cli.json
zu ändern:
{ "$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": {} } }
Authentifizierung
Erinnern Sie sich, wie wir die Klasse AuthGuard
implementiert hatten, um die Routing-Konfiguration festzulegen? Jedes Mal, wenn wir zu einer anderen Seite navigieren, verwenden wir diese Klasse, um zu überprüfen, ob der Benutzer mit einem Token authentifiziert ist. Wenn nicht, leiten wir automatisch zur Anmeldeseite weiter. Die Datei dafür ist canActivateAuthGuard.ts
– erstellen Sie sie im Ordner helpers
und lassen Sie sie so aussehen:
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; } }
Jedes Mal, wenn wir die Seite wechseln, wird also die Methode canActivate
aufgerufen, die überprüft, ob der Benutzer authentifiziert ist, und wenn nicht, verwenden wir unsere Router
-Instanz, um auf die Anmeldeseite umzuleiten. Aber was ist diese neue Methode in der Helper
-Klasse? Lassen Sie uns unter dem Ordner helpers
eine Datei helpers.ts
erstellen. Hier müssen wir localStorage
verwalten, wo wir das Token speichern, das wir vom Backend erhalten.
Notiz
In Bezug auf localStorage können Sie auch Cookies oder sessionStorage , und die Entscheidung hängt von dem Verhalten ab, das wir implementieren möchten. Wie der Name schon sagt, ist sessionStorage nur für die Dauer der Browsersitzung verfügbar und wird gelöscht, wenn der Tab oder das Fenster geschlossen wird; es überlebt jedoch das Neuladen von Seiten. Wenn die von Ihnen gespeicherten Daten ständig verfügbar sein müssen, ist sessionStorage dem localStorage vorzuziehen. Cookies dienen in erster Linie zum serverseitigen Lesen, während localStorage nur clientseitig gelesen werden kann. Die Frage ist also, wer in Ihrer App diese Daten benötigt – der Client oder der 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()); } }
Macht unser Authentifizierungscode jetzt Sinn? Wir kommen später auf die Subject
-Klasse zurück, aber lassen Sie uns jetzt für eine Minute zur Routing-Konfiguration zurückkehren. Schauen Sie sich diese Zeile an:
{ path: 'logout', component: LogoutComponent},
Dies ist unsere Komponente, um sich von der Site abzumelden, und es ist nur eine einfache Klasse, um den localStorage
zu bereinigen. Erstellen wir es im Ordner components/login
mit dem Namen 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']); } }
Jedes Mal, wenn wir zur URL /logout
logout gehen, wird der localStorage
entfernt und die Site wird auf die Anmeldeseite umgeleitet. Zum Schluss erstellen login.component.ts
wie folgt:
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']); }); } }
Wie Sie sehen können, haben wir unsere Anmeldeinformationen hier im Moment hartcodiert. Beachten Sie, dass wir hier eine Dienstklasse aufrufen; Wir werden diese Dienstklassen erstellen, um im nächsten Abschnitt Zugriff auf unser Backend zu erhalten.

Schließlich müssen wir zur Datei app.component.ts
, dem Layout der Site. Wenn der Benutzer authentifiziert ist, werden hier die Menü- und Kopfzeilenabschnitte angezeigt, aber wenn nicht, ändert sich das Layout, um nur unsere Anmeldeseite anzuzeigen.
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(); } }
Erinnern Sie sich an die Subject
-Klasse in unserer Hilfsklasse? Dies ist ein Observable
. Observable
s bieten Unterstützung für die Weitergabe von Nachrichten zwischen Herausgebern und Abonnenten in Ihrer Anwendung. Jedes Mal, wenn sich das Authentifizierungstoken ändert, wird die authentication
aktualisiert. Wenn Sie die Datei app.component.html
, wird es jetzt wahrscheinlich sinnvoller sein:
<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>
Dienstleistungen
An diesem Punkt navigieren wir zu verschiedenen Seiten, authentifizieren unsere Clientseite und rendern ein sehr einfaches Layout. Aber wie können wir Daten vom Backend bekommen? Ich empfehle dringend, den gesamten Back-End-Zugriff insbesondere über Serviceklassen durchzuführen. Unser erster Dienst befindet sich im services
mit dem Namen 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) ); } }
Der erste Aufruf an das Backend ist ein POST-Aufruf an die Token-API. Die Token-API benötigt den Token-String nicht im Header, aber was passiert, wenn wir einen anderen Endpunkt aufrufen? Wie Sie hier sehen können, TokenService
(und Dienstklassen im Allgemeinen) von der BaseService
-Klasse. Schauen wir uns das mal an:
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); } }
Jedes Mal, wenn wir einen HTTP-Aufruf tätigen, implementieren wir den Header der Anfrage einfach mit super.header
. Wenn sich das Token in localStorage
befindet, wird es an den Header angehängt, aber wenn nicht, legen wir einfach das JSON-Format fest. Eine andere Sache, die wir hier sehen können, ist, was passiert, wenn die Authentifizierung fehlschlägt.
Die Anmeldekomponente ruft die Dienstklasse auf und die Dienstklasse ruft das Backend auf. Sobald wir das Token haben, verwaltet die Hilfsklasse das Token, und jetzt können wir die Liste der Benutzer aus unserer Datenbank abrufen.
Um Daten aus der Datenbank abzurufen, stellen Sie zunächst sicher, dass wir die Modellklassen mit den Back-End-Ansichtsmodellen in unserer Antwort abgleichen.
In user.ts
:
export class User { id: number; name: string; }
Und wir können jetzt die Datei user.service.ts
erstellen:
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)); }
Das Backend
Schnellstart
Willkommen beim ersten Schritt unserer Web API Core 2-Anwendung. Als Erstes müssen wir eine ASP.Net Core-Webanwendung erstellen, die wir SeedAPI.Web.API
nennen.
Achten Sie darauf, die leere Vorlage für einen sauberen Start zu wählen, wie Sie unten sehen können:
Das ist alles, wir erstellen die Lösung beginnend mit einer leeren Webanwendung. Jetzt wird unsere Architektur so sein, wie wir sie unten auflisten, also müssen wir die verschiedenen Projekte erstellen:
Klicken Sie dazu einfach mit der rechten Maustaste auf die Lösung und fügen Sie ein Projekt „Klassenbibliothek (.NET Core)“ hinzu.
Die Architektur
Im vorherigen Abschnitt haben wir acht Projekte erstellt, aber wozu dienen sie? Hier ist eine einfache Beschreibung von jedem:
-
Web.API
: Dies ist unser Startprojekt und wo Endpunkte erstellt werden. Hier richten wir JWT, Injektionsabhängigkeiten und Controller ein. -
ViewModels
: Hier führen wir Konvertierungen von den Datentypen durch, die Controller in den Antworten an das Frontend zurückgeben. Es hat sich bewährt, diese Klassen mit den Front-End-Modellen abzugleichen. -
Interfaces
: Dies ist hilfreich bei der Implementierung von Injektionsabhängigkeiten. Der überzeugende Vorteil einer statisch typisierten Sprache besteht darin, dass der Compiler dabei helfen kann, zu überprüfen, ob ein Vertrag, auf den sich Ihr Code stützt, tatsächlich erfüllt wird. -
Commons
: Alle gemeinsam genutzten Verhaltensweisen und Dienstprogrammcodes werden hier sein. -
Models
: Es hat sich bewährt, die Datenbank nicht direkt mit den Front-End-zugewandtenViewModels
, daher besteht der Zweck vonModels
darin, Entitätsdatenbankklassen unabhängig vom Front-End zu erstellen. Dadurch können wir in Zukunft unsere Datenbank ändern, ohne dass sich dies notwendigerweise auf unser Frontend auswirkt. Es hilft auch, wenn wir einfach ein Refactoring durchführen wollen. -
Maps
: Hier ordnen wirViewModels
Models
zu und umgekehrt. Dieser Schritt wird zwischen Controllern und Diensten aufgerufen. -
Services
: Eine Bibliothek zum Speichern der gesamten Geschäftslogik. -
Repositories
: Dies ist der einzige Ort, an dem wir die Datenbank aufrufen.
Die Verweise sehen wie folgt aus:
JWT-basierte Authentifizierung
In diesem Abschnitt sehen wir uns die grundlegende Konfiguration der Token-Authentifizierung an und gehen etwas tiefer auf das Thema Sicherheit ein.
Um mit dem Festlegen des JSON-Web-Tokens (JWT) zu beginnen, erstellen wir die nächste Klasse im App_Start
Ordner mit dem Namen JwtTokenConfig.cs
. Der Code darin sieht folgendermaßen aus:
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(); }); } } }
Die Werte der Validierungsparameter hängen von den Anforderungen des jeweiligen Projekts ab. Den gültigen Benutzer und die gültige Zielgruppe können wir durch Lesen der Konfigurationsdatei appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
Dann müssen wir es nur von der ConfigureServices
-Methode 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(); }
Jetzt können wir unseren ersten Controller namens TokenController.cs
. Der Wert, den wir in appsettings.json
auf "veryVerySecretKey"
, sollte mit dem übereinstimmen, den wir zum Erstellen des Tokens verwenden, aber zuerst erstellen wir das LoginViewModel
in unserem ViewModels
-Projekt:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
Und zum Schluss der 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; } } }
Die BuildToken
Methode erstellt das Token mit dem angegebenen Sicherheitscode. Für die Authenticate
-Methode ist im Moment nur die Benutzervalidierung fest codiert, aber wir müssen die Datenbank aufrufen, um sie am Ende zu validieren.
Der Anwendungskontext
Das Einrichten von Entity Framework ist wirklich einfach, seit Microsoft die Core 2.0-Version – kurz EF Core 2 – auf den Markt gebracht hat. Wir werden mit einem Code-First-Modell unter Verwendung von identityDbContext
in die Tiefe gehen, stellen Sie also zunächst sicher, dass Sie alle Abhängigkeiten installiert haben. Sie können NuGet verwenden, um es zu verwalten:
Mit dem Models
-Projekt können wir hier im Context
-Ordner zwei Dateien erstellen, ApplicationContext.cs
und IApplicationContext.cs
. Außerdem benötigen wir eine EntityBase
-Klasse.
Die EntityBase
Dateien werden von jedem Entitätsmodell geerbt, aber User.cs
ist eine Identitätsklasse und die einzige Entität, die von IdentityUser
erbt. Unten sind beide Klassen:
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(); } } }
Jetzt können wir ApplicationContext.cs
erstellen, die so aussehen wird:
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(); } } } }
Wir sind wirklich nah dran, aber zuerst müssen wir weitere Klassen erstellen, diesmal im Ordner App_Start
, der sich im Web.API
Projekt befindet. Die erste Klasse besteht darin, den Anwendungskontext zu initialisieren, und die zweite besteht darin, Beispieldaten nur zu Testzwecken während der Entwicklung zu erstellen.
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(); }
Abhängigkeitsspritze
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.
Das Maps
Projekt
Dieser Schritt dient nur dazu, ViewModels
zu und von Datenbankmodellen zuzuordnen. Wir müssen eine für jede Entität erstellen, und nach unserem vorherigen Beispiel sieht die Datei UserMap.cs
so aus:
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; } } }
Wieder einmal sieht es so aus, als würde die Abhängigkeitsinjektion im Konstruktor der Klasse funktionieren und Maps mit dem Services-Projekt verknüpfen.
Das Services
Hier gibt es nicht allzu viel zu sagen: Unser Beispiel ist wirklich einfach und wir müssen hier keine Geschäftslogik oder Code schreiben. Dieses Projekt würde sich in zukünftigen fortgeschrittenen Anforderungen als nützlich erweisen, wenn wir vor oder nach den Datenbank- oder Controller-Schritten Berechnungen oder Logik ausführen müssen. Nach dem Beispiel sieht die Klasse ziemlich kahl aus:
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(); } } }
Das Repositories
Projekt
Wir kommen zum letzten Abschnitt dieses Tutorials: Wir müssen nur Aufrufe an die Datenbank senden, also erstellen wir eine UserRepository.cs
-Datei, in der wir Benutzer in der Datenbank lesen, einfügen oder aktualisieren können.
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; } } } }
Zusammenfassung
In diesem Artikel habe ich erklärt, wie man mit Angular 5 und Web API Core 2 eine gute Architektur erstellt. An diesem Punkt haben Sie die Basis für ein großes Projekt mit Code geschaffen, der ein großes Wachstum an Anforderungen unterstützt.
Die Wahrheit ist, nichts konkurriert mit JavaScript im Frontend und was kann mit C# konkurrieren, wenn Sie die Unterstützung von SQL Server und Entity Framework im Backend benötigen? Die Idee dieses Artikels war also, das Beste aus zwei Welten zu kombinieren, und ich hoffe, es hat Ihnen gefallen.
Was kommt als nächstes?
Wenn Sie in einem Team von Angular-Entwicklern arbeiten, könnten wahrscheinlich unterschiedliche Entwickler im Front-End und im Back-End arbeiten, daher könnte eine gute Idee, die Bemühungen beider Teams zu synchronisieren, die Integration von Swagger mit Web API 2 sein. Swagger ist großartig Tool zum Dokumentieren und Testen Ihrer RESTFul-APIs. Lesen Sie den Microsoft-Leitfaden: Erste Schritte mit Swashbuckle und ASP.NET Core.
Wenn Sie noch sehr neu bei Angular 5 sind und Schwierigkeiten haben, ihm zu folgen, lesen Sie An Angular 5 Tutorial: Step by Step Guide to Your First Angular 5 App von Toptaler Sergey Moiseev.