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 -vnpm -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

第一次构建 Angular 应用程序

将创建一个名为dist的新文件夹,我们可以使用 IIS 或您喜欢的任何 Web 服务器为其提供服务。 然后您可以在浏览器中输入 URL,然后……完成!

新的目录结构

笔记

本教程的目的不是展示如何设置 Web 服务器,因此我假设您已经具备这些知识。

Angular 5 欢迎屏幕

src文件夹

src 文件夹结构

我的src文件夹的结构如下:在app文件夹中,我们有components ,我们将为每个 Angular 组件创建csstsspechtml文件。 我们还将创建一个config文件夹来保存站点配置, directives将包含我们所有的自定义指令, helpers程序将包含像身份验证管理器这样的公共代码, layout将包含主要组件,如主体、头部和侧面板, models将保存什么与后端视图模型相匹配,最终services将拥有对后端的所有调用的代码。

app文件夹之外,我们将保留默认创建的文件夹,例如assetsenvironments ,以及根文件。

创建配置文件

让我们在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导入RouterModuleRoutes ,我们就可以映射我们想要实现的路径。 在这里,我们创建了四个路径:

  • /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.tshelpers文件夹中创建它,让它看起来像这样:

 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仅在浏览器会话期间可用,并在关闭选项卡或窗口时被删除; 但是,它确实可以在页面重新加载后幸存下来。 如果您存储的数据需要持续可用,那么localStoragesessionStorage更可取。 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类吗? 这是一个ObservableObservable支持在应用程序中的发布者和订阅者之间传递消息。 每次身份验证令牌更改时, 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)”项目即可。

添加“类库(.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.csIApplicationContext.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 应用程序的分步指南。