Angular 5 和 ASP.NET Core
已發表: 2022-03-11自從 Angular 的第一個版本在客戶端幾乎扼殺了微軟以來,我一直在考慮寫一篇博文。 ASP.Net、Web Forms 和 MVC Razor 等技術已經過時,取而代之的是不完全是 Microsoft 的 JavaScript 框架。 然而,自從 Angular 的第二個版本以來,微軟和谷歌一直在合作創建 Angular 2,這也是我最喜歡的兩種技術開始合作的時候。
在這個博客中,我想幫助人們創建結合這兩個世界的最佳架構。 你準備好了嗎? 開始了!
關於架構
您將構建一個使用 RESTful Web API Core 2 服務的 Angular 5 客戶端。
客戶端:
- 角 5
- 角 CLI
- 角材料
服務器端:
- .NET C# Web API 核心 2
- 注入依賴
- 智威湯遜認證
- 實體框架代碼先
- SQL 服務器
筆記
在這篇博文中,我們假設讀者已經具備 TypeScript、Angular 模塊、組件和導入/導出的基本知識。 這篇文章的目標是創建一個允許代碼隨時間增長的良好架構。 |
你需要什麼?
讓我們從選擇 IDE 開始。 當然,這只是我的偏好,你可以使用你覺得更舒服的那個。 就我而言,我將使用 Visual Studio Code 和 Visual Studio 2017。
為什麼有兩個不同的 IDE? 由於微軟為前端創建了 Visual Studio Code,我無法停止使用這個 IDE。 無論如何,我們還將了解如何將 Angular 5 集成到解決方案項目中,如果您是那種喜歡只用一個 F5 調試後端和前端的開發人員,這將對您有所幫助。
關於後端,您可以安裝最新的 Visual Studio 2017 版本,該版本為開發人員提供免費版本,但非常完整:社區。
因此,這裡列出了我們需要為本教程安裝的東西:
- 視覺工作室代碼
- Visual Studio 2017 社區(或任何)
- Node.js v8.10.0
- SQL 服務器 2017
筆記
通過在終端或控制台窗口中運行node -v 和npm -v 來驗證您至少運行的是 Node 6.9.x 和 npm 3.xx。 舊版本會產生錯誤,但新版本很好。 |
前端
快速開始
讓樂趣開始! 我們需要做的第一件事是全局安裝 Angular CLI,因此打開 node.js 命令提示符並運行以下命令:
npm install -g @angular/cli
好的,現在我們有了模塊捆綁器。 這通常會在您的用戶文件夾下安裝模塊。 默認情況下不需要別名,但如果需要,可以執行下一行:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
下一步是創建新項目。 我將其稱為angular5-app
。 首先,我們導航到要在其下創建站點的文件夾,然後:
ng new angular5-app
首次構建
雖然您可以只運行ng serve --open
來測試您的新網站,但我建議您從您最喜歡的 Web 服務中測試該網站。 為什麼? 好吧,有些問題只能在生產中發生,而使用ng build
站點是接近此環境的最接近的方法。 然後我們可以使用 Visual Studio Code 打開文件夾angular5-app
並在終端 bash 上運行ng build
:
將創建一個名為dist
的新文件夾,我們可以使用 IIS 或您喜歡的任何 Web 服務器為其提供服務。 然後您可以在瀏覽器中輸入 URL,然後……完成!
筆記
本教程的目的不是展示如何設置 Web 服務器,因此我假設您已經具備這些知識。 |
src
文件夾
我的src
文件夾的結構如下:在app
文件夾中,我們有components
,我們將為每個 Angular 組件創建css
、 ts
、 spec
和html
文件。 我們還將創建一個config
文件夾來保存站點配置, directives
將包含我們所有的自定義指令, helpers
程序將包含像身份驗證管理器這樣的公共代碼, layout
將包含主要組件,如主體、頭部和側面板, models
將保存什麼與後端視圖模型相匹配,最終services
將擁有對後端的所有調用的代碼。
在app
文件夾之外,我們將保留默認創建的文件夾,例如assets
和environments
,以及根文件。
創建配置文件
讓我們在config
文件夾中創建一個config.ts
文件並調用AppConfig
類。 這是我們可以設置我們將在代碼中不同位置使用的所有值的地方; 例如,API 的 URL。 請注意,該類實現了一個get
屬性,該屬性接收作為參數的鍵/值結構和一個簡單的方法來訪問相同的值。 這樣,很容易從繼承自它的類中獲取調用this.config.setting['PathAPI']
的值。
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]; } };
角材料
在開始佈局之前,讓我們設置 UI 組件框架。 當然,你可以使用其他的像 Bootstrap,但是如果你喜歡 Material 的樣式,我推薦它,因為它也得到了 Google 的支持。
要安裝它,我們只需要運行接下來的三個命令,我們可以在 Visual Studio Code 終端上執行它們:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
第二個命令是因為一些 Material 組件依賴於 Angular Animations。 我還建議閱讀官方頁面以了解支持哪些瀏覽器以及什麼是 polyfill。
第三個命令是因為一些 Material 組件依賴於 HammerJS 的手勢。
現在我們可以繼續在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 ],
下一步是更改style.css
文件,添加您要使用的主題類型:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
現在通過在main.ts
文件中添加這一行來導入 HammerJS:
import 'hammerjs';
最後,我們所缺少的是將 Material 圖標添加到index.html
的 head 部分:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
佈局
在本例中,我們將創建一個簡單的佈局,如下所示:
這個想法是通過單擊標題上的某個按鈕來打開/隱藏菜單。 Angular Responsive 將為我們完成剩下的工作。 為此,我們將創建一個layout
文件夾並將默認創建的app.component
文件放入其中。 但我們還將為佈局的每個部分創建相同的文件,如下圖所示。 然後, app.component
將是主體, head.component
是標題,而left-panel.component
是菜單。
現在讓我們更改app.component.html
如下:
<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>
基本上,我們將在組件中有一個authentication
屬性,如果用戶未登錄,我們可以刪除標題和菜單,而是顯示一個簡單的登錄頁面。
head.component.html
看起來像這樣:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
只需一個按鈕即可註銷用戶——我們稍後會再次討論這個問題。 至於left-panel.component.html
,現在只需將 HTML 更改為:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
我們保持簡單:到目前為止,只有兩個鏈接可以瀏覽兩個不同的頁面。 (我們稍後也會回到這個。)
現在,這就是頭部和左側組件 TypeScript 文件的樣子:
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'; }
但是app.component
的 TypeScript 代碼呢? 我們這裡留個小謎團,暫停一會,實現鑑權後再回過頭來。
路由
好的,現在我們有了 Angular Material 幫助我們處理 UI 和一個簡單的佈局來開始構建我們的頁面。 但是我們如何在頁面之間導航呢?
為了創建一個簡單的示例,讓我們創建兩個頁面:“用戶”,我們可以在其中獲取數據庫中現有用戶的列表,以及“儀表板”,我們可以顯示一些統計信息的頁面。
在app
文件夾中,我們將創建一個名為app-routing.modules.ts
的文件,如下所示:
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 {}
就這麼簡單:只需從@angular/router
導入RouterModule
和Routes
,我們就可以映射我們想要實現的路徑。 在這裡,我們創建了四個路徑:
-
/dashboard
: 我們的主頁 /login
: 用戶可以認證的頁面/logout
:註銷用戶的簡單路徑/users
:我們想要從後端列出用戶的第一頁
注意dashboard
默認是我們的頁面,所以如果用戶輸入URL /
,頁面會自動重定向到這個頁面。 另外,看一下canActivate
參數:這裡我們創建了對類AuthGuard
的引用,這將允許我們檢查用戶是否登錄。如果沒有,它會重定向到登錄頁面。 在下一節中,我將向您展示如何創建此類。
現在,我們需要做的就是創建菜單。 還記得我們在佈局部分創建left-panel.component.html
文件時的樣子嗎?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
這是我們的代碼與現實相遇的地方。 現在我們可以構建代碼並在 URL 中測試它:您應該能夠從 Dashboard 頁面導航到 Users,但是如果您直接在瀏覽器中鍵入our.site.url/users
會發生什麼?
請注意,如果您在通過應用程序的側面板成功導航到該 URL 後刷新瀏覽器,也會出現此錯誤。 要理解這個錯誤,請允許我參考官方文檔,其中非常清楚:
路由應用程序應支持深度鏈接。 深層鏈接是一個 URL,它指定應用程序內部組件的路徑。 例如,
http://www.mysite.com/users/42
是一個指向英雄詳情頁面的深層鏈接,該頁面顯示 id: 42 的英雄。當用戶從正在運行的客戶端導航到該 URL 時,沒有問題。 Angular 路由器解釋 URL 並路由到該頁面和英雄。
但是單擊電子郵件中的鏈接,在瀏覽器地址欄中輸入鏈接,或者只是在英雄詳細信息頁面上刷新瀏覽器——所有這些操作都由瀏覽器本身處理,在運行的應用程序之外。 瀏覽器繞過路由器直接向服務器請求該 URL。靜態服務器在收到對
http://www.mysite.com/
的請求時通常會返回 index.html。 但它拒絕http://www.mysite.com/users/42
並返回 404 - Not Found 錯誤,除非它被配置為返回 index.html。
要解決這個問題很簡單,我們只需要創建服務提供者文件配置。 由於我在這裡使用 IIS,我將向您展示如何在此環境中執行此操作,但是對於 Apache 或任何其他 Web 服務器來說,這個概念是相似的。
因此,我們在src
文件夾中創建了一個名為web.config
的文件,如下所示:
<?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>
然後我們需要確保該資產將被複製到已部署的文件夾中。 我們需要做的就是更改我們的 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": {} } }
驗證
你還記得我們是如何實現類AuthGuard
來設置路由配置的嗎? 每次我們導航到不同的頁面時,我們都將使用這個類來驗證用戶是否使用令牌進行了身份驗證。 如果沒有,我們將自動重定向到登錄頁面。 這個文件是canActivateAuthGuard.ts
在helpers
文件夾中創建它,讓它看起來像這樣:
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; } }
所以每次我們改變頁面時都會調用canActivate
方法,它會檢查用戶是否通過了身份驗證,如果沒有,我們使用我們的Router
實例重定向到登錄頁面。 但是Helper
類的這個新方法是什麼? 在helpers
文件夾下,讓我們創建一個文件helpers.ts
。 在這裡,我們需要管理localStorage
,我們將從後端獲取的令牌存儲在其中。
筆記
關於localStorage ,您也可以使用 cookie 或sessionStorage ,這取決於我們想要實現的行為。 顧名思義, sessionStorage 僅在瀏覽器會話期間可用,並在關閉選項卡或窗口時被刪除; 但是,它確實可以在頁面重新加載後倖存下來。 如果您存儲的數據需要持續可用,那麼localStorage 比sessionStorage 更可取。 Cookie 主要用於讀取服務器端,而localStorage 只能讀取客戶端。 所以問題是,在您的應用程序中,誰需要這些數據——客戶端還是服務器? |
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()); } }
我們的驗證碼現在有意義嗎? 稍後我們將回到Subject
類,但現在讓我們回到路由配置一分鐘。 看看這一行:
{ path: 'logout', component: LogoutComponent},
這是我們登出站點的組件,它只是一個簡單的類來清除localStorage
。 讓我們在components/login
文件夾下創建它,名稱為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']); } }
所以每次我們訪問 URL /logout
時, localStorage
都會被移除,網站會重定向到登錄頁面。 最後,讓我們像這樣創建login.component.ts
:
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']); }); } }
如您所見,目前我們已在此處硬編碼我們的憑據。 請注意,這裡我們調用的是服務類; 我們將在下一節創建這些服務類來訪問我們的後端。
最後,我們需要回到app.component.ts
文件,站點的佈局。 在這裡,如果用戶通過了身份驗證,它將顯示菜單和標題部分,但如果沒有,佈局將更改為僅顯示我們的登錄頁面。
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(); } }
還記得我們助手類中的Subject
類嗎? 這是一個Observable
。 Observable
支持在應用程序中的發布者和訂閱者之間傳遞消息。 每次身份驗證令牌更改時, authentication
屬性都會更新。 查看app.component.html
文件,現在可能會更有意義:
<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>
服務
在這一點上,我們正在導航到不同的頁面,驗證我們的客戶端,並呈現一個非常簡單的佈局。 但是我們如何從後端獲取數據呢? 我強烈建議特別從服務類進行所有後端訪問。 我們的第一個服務將位於services
文件夾中,名為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) ); } }
對後端的第一次調用是對令牌 API 的 POST 調用。 令牌 API 不需要標頭中的令牌字符串,但是如果我們調用另一個端點會發生什麼? 正如您在此處看到的, TokenService
(以及一般的服務類)繼承自BaseService
類。 讓我們來看看這個:
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); } }
所以每次我們進行 HTTP 調用時,我們只使用super.header
來實現請求的頭部。 如果令牌在localStorage
中,那麼它將附加在標頭中,但如果不是,我們將設置 JSON 格式。 我們在這裡可以看到的另一件事是如果身份驗證失敗會發生什麼。
登錄組件會調用服務類,服務類會調用後端。 一旦我們有了令牌,助手類將管理令牌,現在我們準備從我們的數據庫中獲取用戶列表。
要從數據庫中獲取數據,首先要確保我們將模型類與響應中的後端視圖模型相匹配。
在user.ts
:
export class User { id: number; name: string; }
我們現在可以創建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)); }
後端
快速開始
歡迎來到我們的 Web API Core 2 應用程序的第一步。 我們需要做的第一件事是創建一個 ASP.Net Core Web 應用程序,我們將其稱為SeedAPI.Web.API
。
請務必選擇 Empty 模板進行全新啟動,如下所示:
就是這樣,我們從一個空的 Web 應用程序開始創建解決方案。 現在我們的架構將如下所示,因此必須創建不同的項目:
為此,只需右鍵單擊解決方案並添加“類庫 (.NET Core)”項目即可。
架構
在上一節中,我們創建了八個項目,但它們是做什麼用的? 以下是對每一個的簡單描述:
-
Web.API
:這是我們的啟動項目,也是創建端點的地方。 在這裡,我們將設置 JWT、注入依賴項和控制器。 -
ViewModels
:在這裡,我們從控制器將在響應中返回到前端的數據類型執行轉換。 將這些類與前端模型相匹配是一種很好的做法。 -
Interfaces
:這將有助於實現注入依賴。 靜態類型語言的引人注目的好處是編譯器可以幫助驗證您的代碼所依賴的契約是否真正得到滿足。 -
Commons
:所有共享行為和實用程序代碼都將在這裡。 -
Models
:最好不要將數據庫直接與面向前端的ViewModels
匹配,因此Models
的目的是創建獨立於前端的實體數據庫類。 這將使我們將來能夠更改我們的數據庫,而不必對我們的前端產生影響。 當我們只是想做一些重構時,它也很有幫助。 -
Maps
:這是我們將ViewModels
映射到Models
的地方,反之亦然。 此步驟在控制器和服務之間調用。 -
Services
:存儲所有業務邏輯的庫。 -
Repositories
:這是我們調用數據庫的唯一地方。
參考將如下所示:
基於 JWT 的身份驗證
在本節中,我們將看到令牌身份驗證的基本配置,並深入探討安全性主題。
要開始設置 JSON Web 令牌 (JWT),讓我們在App_Start
文件夾中創建名為JwtTokenConfig.cs
的下一個類。 裡面的代碼將如下所示:
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(); }); } } }
驗證參數的值將取決於每個項目的要求。 我們可以設置讀取配置文件appsettings.json
的有效用戶和受眾:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
然後我們只需要從startup.cs
中的ConfigureServices
方法調用它:
// 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(); }
現在我們準備創建我們的第一個控制器,稱為TokenController.cs
。 我們在appsettings.json
中設置為"veryVerySecretKey"
的值應該與我們用於創建令牌的值相匹配,但首先,讓我們在ViewModels
項目中創建LoginViewModel
:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
最後是控制器:
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; } } }
BuildToken
方法將使用給定的安全代碼創建令牌。 Authenticate
方法目前只是硬編碼了用戶驗證,但我們最終需要調用數據庫來驗證它。
應用程序上下文
自從微軟推出了 Core 2.0 版本——簡稱EF Core 2以來,設置 Entity Framework 真的很容易。 我們將使用identityDbContext
深入研究代碼優先模型,因此首先要確保您已安裝所有依賴項。 你可以使用 NuGet 來管理它:
使用Models
項目,我們可以在Context
文件夾中創建兩個文件, ApplicationContext.cs
和IApplicationContext.cs
。 此外,我們將需要一個EntityBase
類。
EntityBase
文件將被每個實體模型繼承,但User.cs
是一個身份類,並且是唯一將從IdentityUser
繼承的實體。 下面是兩個類:
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(); } } }
現在我們準備創建ApplicationContext.cs
,它看起來像這樣:
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(); } } } }
我們真的很接近,但首先我們需要創建更多類,這次是在Web.API
項目中的App_Start
文件夾中。 第一類是初始化應用程序上下文,第二類是創建示例數據,僅用於開發期間的測試目的。
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(); }
依賴注入
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.
Maps
項目
這一步只是將ViewModels
映射到數據庫模型和從數據庫模型映射。 我們必須為每個實體創建一個,並且按照我們之前的示例, UserMap.cs
文件將如下所示:
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; } } }
看起來,依賴注入再次在類的構造函數中工作,將 Maps 鏈接到 Services 項目。
Services
項目
這裡就不多說了:我們的例子真的很簡單,這裡沒有業務邏輯和代碼要寫。 當我們需要在數據庫或控制器步驟之前或之後計算或執行一些邏輯時,該項目將在未來的高級需求中證明是有用的。 按照這個例子,這個類看起來很簡單:
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(); } } }
Repositories
項目
我們將進入本教程的最後一部分:我們只需要調用數據庫,因此我們創建了一個UserRepository.cs
文件,我們可以在其中讀取、插入或更新數據庫中的用戶。
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; } } } }
概括
在本文中,我解釋瞭如何使用 Angular 5 和 Web API Core 2 創建一個良好的架構。此時,您已經為一個大型項目創建了基礎,該項目的代碼支持需求的大幅增長。
事實上,在前端沒有什麼能與 JavaScript 競爭,如果後端需要 SQL Server 和 Entity Framework 的支持,有什麼能與 C# 競爭呢? 所以這篇文章的想法是結合兩個世界的精華,我希望你喜歡它。
下一步是什麼?
如果您在一個 Angular 開發人員團隊中工作,可能會有不同的開發人員在前端和後端工作,因此同步兩個團隊的工作的一個好主意可能是將 Swagger 與 Web API 2 集成。Swagger 是一個很棒的記錄和測試 RESTFul API 的工具。 閱讀 Microsoft 指南:開始使用 Swashbuckle 和 ASP.NET Core。
如果您對 Angular 5 還很陌生並且在繼續學習時遇到困難,請閱讀由 Toptaler Sergey Moiseev 撰寫的 Angular 5 教程:您的第一個 Angular 5 應用程序的分步指南。