作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Pablo Albella's profile image

Pablo Albella

Pablo是一个天才的JavaScript和 .NET developer. 在过去的十年里,他成功地创造了许多复杂的产品.

Previously At

Globant
Share

自从Angular的第一个版本几乎在客户端杀死了微软之后,我就一直在考虑写一篇博文. Technologies like ASP.NET, Web Forms, 和MVC Razor已经过时了, 取而代之的是一个不完全是微软的JavaScript框架. However, 从Angular第二版开始, 微软和谷歌一直在合作开发Angular 2, 这就是我最喜欢的两项技术开始合作的时候, 允许使用Angular的 .NET Core.

在这篇博客中,我想帮助人们创建结合ASP的最佳架构.NET Core with Angular. Are you ready? Here we go!

About the Angular 5/ASP.NET Core Architecture

你将构建一个使用RESTful Web API Core 2服务的Angular 5客户端.

The client side:

  • Angular 5
  • Angular CLI
  • Angular Material

The server side:

  • .NET C# Web API Core 2
  • Injection dependencies
  • JWT authentication
  • Entity framework code first
  • SQL Server

Note

在这篇博文中,我们假设读者已经掌握了TypeScript的基本知识, Angular modules, components, and importing/exporting. 这篇文章的目标是创建一个好的架构,允许代码随着时间的推移而增长.

What Do You Need for Your .NET/Angular project?

让我们从选择IDE开始. 当然,这只是我的偏好,你可以用你觉得更舒服的那个. 在我的情况下,我将使用Visual Studio Code和Visual Studio 2017.

Why two different IDEs? 由于微软为前端创建了Visual Studio Code,我无法停止使用这个IDE. Anyway, 我们还将看到如何在解决方案项目中集成Angular 5, 如果你是那种喜欢用一个F5同时调试后端和前端的开发人员,这将对你有所帮助.

About the back end, 您可以安装最新的Visual Studio 2017版本,该版本为开发人员提供免费版本,但非常完整:Community.

下面是本教程需要安装的东西:

Note

验证您至少正在运行Node 6.9.x and npm 3.x.x by running node -v and npm -v 在终端或控制台窗口中. 旧版本会产生错误,但新版本没有问题.

The Front End

Quick Start

Let the fun begin! 我们需要做的第一件事是全局安装Angular CLI,所以打开这个节点.Js命令提示符并运行此命令:

npm install -g @angular/cli

好了,现在我们有了模块捆绑器. 这通常将模块安装在用户文件夹下. 默认情况下,别名不应该是必需的,但如果你需要它,你可以执行下一行:

alias ng="/.npm / lib / node_modules angular-cli / bin / ng”

下一步是创建新项目. I will call it angular5-app. 首先,我们导航到我们想要创建站点的文件夹,然后:

ng new angular5-app  

First Build

而你可以测试你的新网站只是运行 ng serve --open,我建议你用你最喜欢的网络服务测试一下这个网站. Why? 好吧,有些问题只会发生在生产和构建网站 ng build 最接近这种环境的方法是什么. Then we can open the folder angular5-app with Visual Studio Code and run ng build on the terminal bash:

第一次构建angular应用

A new folder called dist 将被创建,我们可以使用IIS或您喜欢的任何web服务器提供它. 然后您可以在浏览器中输入URL,然后……完成!

the new directory structure

Note

本教程的目的不是展示如何设置web服务器, 所以我想你已经知道了.

Angular 5 Welcome Screen

The src Folder

The src folder structure

My src 文件夹的结构如下 app folder we have components 在这里,我们将为每个Angular组件创建 css, ts, spec, and html files. We will also create a config 文件夹保存站点配置, directives 会有我们所有的自定义指令, helpers 将容纳通用代码,如身份验证管理器, layout 将包含主体、头部和侧板等主要部件, models 保留与后端视图模型匹配的内容,最后 services 会有所有呼叫后端的代码吗.

Outside the app 文件夹我们将保留默认创建的文件夹,如 assets and environments, and also the root files.

创建配置文件

Let’s create a config.ts file inside our config folder and call the class AppConfig. This is where we can set all the values we will use in different places in our code; for instance, the URL of the API. 注意,这个类实现了 get property which receives, as a parameter, 键/值结构和访问相同值的简单方法. 这样,只需调用就可以很容易地获得值 this.config.setting['PathAPI'] 从从它继承的类.

从“@angular/core”中导入{Injectable};
@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];
    }
};

Angular Material

在开始布局之前,让我们先设置UI组件框架. Of course, 你可以使用其他工具,比如Bootstrap, 但如果你喜欢材料的样式, 我推荐它,因为谷歌也支持它.

To install it, 我们只需要运行接下来的三个命令, 我们可以在Visual Studio Code终端上执行:

NPM install——save @angular/material @angular/cdk
NPM install——save @angular/animations
npm install --save hammerjs

第二个命令是因为一些材质组件依赖于Angular动画. I also recommend reading the official page 了解支持哪些浏览器以及什么是polyfill.

第三个命令是因为一些Material组件依赖于HammerJS的手势.

现在我们可以继续导入我们想要在我们的 app.module.ts file:

从“@angular/material”中导入{MatButtonModule, MatCheckboxModule};
从“@angular/material/input”中导入{MatInputModule};
从“@angular/material/form-field”中导入{MatFormFieldModule};
从“@angular/material/sidenav”中导入{MatSidenavModule};
// ...
@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatButtonModule, 
    MatCheckboxModule,
    MatInputModule,
    MatFormFieldModule,
    MatSidenavModule,
    AppRoutingModule,
    HttpClientModule
  ],

Next step is to change the style.css 文件,添加你想要使用的主题类型:

@ import”~ @angular /材料/的预构建主题/ deeppurple-amber.css";

文件中添加这一行来导入HammerJS main.ts file:

import 'hammerjs';

最后我们缺少的是添加材质图标到 index.html, inside the head section:


The Layout

在这个例子中,我们将创建一个简单的布局,像这样:

Layout example

这个想法是通过点击标题上的一些按钮来打开/隐藏菜单. Angular Responsive会帮我们完成剩下的工作. To do this we will create a layout folder and put inside it the app.component files created by default. 但我们也会为布局的每个部分创建相同的文件,就像你在下一张图中看到的那样. Then, app.component will be the body, head.component the header, and left-panel.component the menu.

Highlighted config folder

Now let’s change app.component.html as follows:

Basically we will have an authentication 属性,它允许我们在用户未登录时删除标题和菜单, and instead, show a simple login page.

The head.component.html looks like this:

{{title}}

只需一个按钮就可以注销用户——我们稍后再来讨论这个问题. As for left-panel.component.html,现在只需将HTML更改为:


我们保持了它的简单性:到目前为止,它只有两个链接来导航两个不同的页面. (我们稍后还会回到这个问题.)

现在,这就是头部和左侧组件TypeScript文件的样子:

从“@angular/core”中导入{Component};
@Component({
  selector: 'app-head',
  templateUrl: './head.component.html',
  styleUrls: ['./head.component.css']
})
export class HeadComponent {
  title = 'Angular 5 Seed';
}
从“@angular/core”中导入{Component};
@Component({
  selector: 'app-left-panel',
  templateUrl: './left-panel.component.html',
  styleUrls: ['./left-panel.component.css']
})
导出类LeftPanelComponent {
  title = 'Angular 5 Seed';
}

但是TypeScript的代码呢 app.component? 我们在这里留下一个小谜团,暂停一下, 在实现身份验证之后再回到这个.

Routing

Okay, 现在我们有了Angular Material来帮助我们创建UI和一个简单的布局,开始构建我们的页面. 但是我们如何在页面之间导航呢?

为了创建一个简单的例子, let’s create two pages: “User,,我们可以在其中获得数据库中现有用户的列表, and “Dashboard,这个页面可以显示一些统计数据.

Inside the app 文件夹,我们将创建一个名为 app-routing.modules.ts looking like this:

从“@angular/core”中导入{NgModule};
从“@angular/router”中导入{RouterModule, Routes};
导入{AuthGuard}./帮助/ canActivateAuthGuard”;
导入{LoginComponent}./components/login/login.component';
导入{LogoutComponent}./components/login/logout.component';
导入{DashboardComponent}./组件/仪表板/仪表盘.component';
导入{UsersComponent}./components/users/users.component';
const routes: Routes = [
  {path: ", redirectTo: '/dashboard', pathMatch: 'full', canActivate: [AuthGuard]},
  {path: 'login',组件:LoginComponent},
  {path: 'logout', component: LogoutComponent},
  {path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard]},
  {path: 'users',组件:UsersComponent,canActivate: [AuthGuard]}
];
@NgModule({
  imports: [ RouterModule.forRoot(routes) ],
  exports: [ RouterModule ]
})
导出类AppRoutingModule {}

就是这么简单:只是导入 RouterModule and Routes from @angular/router,我们可以映射我们想要实现的路径. 这里我们创建了四条路径:

  • /dashboard: Our home page
  • /login:用户可以进行身份验证的页面
  • /logout:用于注销用户的简单路径
  • /users:我们要列出后端的用户的第一页

Note that dashboard 默认情况下是我们的页面,所以如果用户输入URL /,该页将自动重定向到该页. Also, take a look at the canActivate 这里我们创建了对类的引用 AuthGuard,这将允许我们检查用户是否已登录. 如果没有,它将重定向到登录页面. 在下一节中,我将展示如何创建这个类.

现在,我们要做的就是创建菜单. 还记得在布局部分中我们创建 left-panel.component.html file to look like this?


这里是我们的代码遇到现实的地方. 现在我们可以构建代码并在URL中进行测试:您应该能够从Dashboard页面导航到Users, 但如果你输入URL会发生什么呢 our.site.url/users in the browser directly?

image alt text

注意,如果你在通过应用的侧面板成功导航到该URL后刷新浏览器,也会出现此错误. 要理解这个错误,请允许我参考 the official docs where it is really clear:

路由应用程序应该支持深度链接. 深层链接是一个URL,它指定了应用中某个组件的路径. For example, http://www.mysite.com/users/42 是否有一个指向英雄详情页面的深链接,显示id: 42的英雄.

当用户从正在运行的客户机中导航到该URL时不会出现问题. Angular的路由器会解释URL,并路由到该页面和英雄.

但是点击邮件中的链接, 在浏览器地址栏中输入, 或者只是在英雄详情页面上刷新浏览器——所有这些操作都是由浏览器自己处理的, 在运行的应用程序之外. 浏览器绕过路由器,直接向服务器请求该URL.

静态服务器通常返回索引.的请求 http://www.mysite.com/. But it rejects http://www.mysite.com/users/42 并返回404 - Not Found错误,除非它被配置为返回index.html instead.

要解决这个问题非常简单,我们只需要创建服务提供者文件配置. 因为我在这里使用IIS, 我将向您展示如何在这种环境中进行操作, 但是这个概念对于Apache或任何其他web服务器都是类似的.

我们在里面创建一个文件 src folder called web.config that looks like this:



  
  
    
      
        
        
          
          
        
        
      
    
  

  
    
  

然后我们需要确保这个资产将被复制到部署的文件夹中. 我们需要做的就是修改Angular CLI的设置文件 angular-cli.json:

{
  "$schema": "./ node_modules @angular / cli / lib / config /模式.json",
  "project": {
    "name": "angular5-app"
  },
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico",
        "web.//或者你的web服务器需要的任何等价的东西
      ],
      "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环境/环境.ts",
      "environments": {
        “开发”:“环境/环境.ts",
        “刺激”:“环境/环境.prod.ts"
      }
    }
  ],
  "e2e": {
    "protractor": {
      "config": "./protractor.conf.js"
    }
  },
  "lint": [
    {
      "project": "src/tsconfig.app.json",
      “排除”:“* * / node_modules / * *”
    },
    {
      "project": "src/tsconfig.spec.json",
      “排除”:“* * / node_modules / * *”
    },
    {
      "project": "e2e/tsconfig.e2e.json",
      “排除”:“* * / node_modules / * *”
    }
  ],
  "test": {
    "karma": {
      "config": "./karma.conf.js"
    }
  },
  "defaults": {
    "styleExt": "css",
    "component": {}
  }
}

Authentication

你还记得我们是怎么上课的吗 AuthGuard 实现设置路由配置? 每次导航到不同的页面时,我们都将使用该类来验证用户是否使用令牌进行了身份验证. 如果没有,我们将自动重定向到登录页面. The file for this is canActivateAuthGuard.ts—create it inside the helpers 文件夹,让它看起来像这样:

从“@angular/ Router”中导入{CanActivate, Router};
从“@angular/core”中导入{Injectable};
从'rxjs/Observable'中导入{Observable};
import { Helpers } from './helpers';
从“@angular/router”中导入{ActivatedRouteSnapshot, RouterStateSnapshot};
@Injectable()
导出类AuthGuard实现CanActivate {
  构造函数(私有路由器:router,私有helper: Helpers) {}
  canActivate(路线:ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean {
    if (!this.helper.isAuthenticated()) {
      this.router.navigate(['/login']);
      return false;
    }
    return true;
  }
}

每次我们改变页面,方法 canActivate 将调用,它将检查用户是否经过身份验证,如果没有,则使用 Router 实例以重定向到登录页面. 但是这个新方法是什么 Helper class? Under the helpers folder let’s create a file helpers.ts. Here we need to manage localStorage,我们将存储从后端获得的令牌.

Note

Regarding localStorage, you can also use cookies or sessionStorage,而决策将取决于我们想要实现的行为. As the name suggests, sessionStorage 仅在浏览器会话期间可用, and is deleted when the tab or window is closed; it does, however, survive page reloads. 如果您存储的数据需要持续可用,那么 localStorage is preferable to sessionStorage. cookie主要用于读取服务器端数据,而 localStorage can only be read client-side. 问题是,在你的应用中,谁需要这些数据,客户端还是服务器?


从“@angular/core”中导入{Injectable};
从'rxjs'中导入{Observable};
从'rxjs/Subject'导入{Subject};
@Injectable()
export class Helpers  {
    private authenticationChanged = new Subject();
    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());
    }
}

我们的认证代码现在有意义了吗? We’ll come back to the Subject 类,但现在让我们花一分钟回到路由配置. Take a look at this line:

 {path: 'logout', component: LogoutComponent},

这是我们注销站点的组件,它只是一个简单的类来清除 localStorage. Let’s create it under the components/login folder with the name of logout.component.ts:

从“@angular/core”中导入{Component, OnInit};
从“@angular/ Router”中导入{Router};
import { Helpers } from '../../helpers/helpers';
@Component({
  selector: 'app-logout',
  template:'' 
})
导出类LogoutComponent实现OnInit {
  构造函数(私有路由器:路由器,私有帮手:帮手){}
  ngOnInit() {
    this.helpers.logout();
    this.router.navigate(['/login']);
  }
}

So every time we go to the URL /logout, the localStorage 将被删除,网站将重定向到登录页面. Finally, let’s create login.component.ts like this:

从“@angular/core”中导入{Component, OnInit};
从“@angular/ Router”中导入{Router};
import { TokenService } from '../../services/token.service';
import { Helpers } from '../../helpers/helpers';
@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: [ './login.component.css' ]
})
导出类LoginComponent实现OnInit {
  构造器(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']);
    });
  }
} 

如您所见,目前我们已经在这里硬编码了我们的凭据. Note that here we are calling a service class; we will create these services classes to get access to our back end in the next section.

最后,我们需要回到 app.component.ts file, the layout of the site. Here, if the user is authenticated, 它将显示菜单和标题部分, but if not, 布局将改变为只显示我们的登录页面.

导出类AppComponent实现AfterViewInit {
  subscription: Subscription;
  authentication: boolean;
  构造函数(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();
  }
}

Remember the Subject class in our helper class? This is an Observable. ObservableS支持在应用程序中的发布者和订阅者之间传递消息. 每次身份验证令牌更改时 authentication property will be updated. Reviewing the app.component.html 文件,现在可能会更有意义:

Services

此时,我们正在导航到不同的页面, authenticating our client side, 渲染一个非常简单的布局. 但我们如何从后端获取数据? 我强烈建议从 service classes in particular. 我们的第一个服务将在 services folder, called token.service.ts:

从“@angular/core”中导入{Injectable};
从“@angular/common/http”中导入{HttpClient, HttpHeaders};
从'rxjs/Observable'中导入{Observable};
从'rxjs/observable/of'中导入{of};
从rxjs/operators中导入{catchError, map, tap};
import { AppConfig } from '../config/config';
import { BaseService } from './base.service';
import { Token } from '../models/token';
import { Helpers } from '../helpers/helpers';
@Injectable()
导出类TokenService扩展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 {
    return this.http.post(this.pathAPI + 'token', body, super.header()).pipe(
        catchError(super.handleError)
      );
  }
}

对后端的第一个调用是对令牌API的POST调用. 令牌API不需要标头中的令牌字符串, 但是如果我们调用另一个端点? As you can see here, TokenService (以及一般的服务类)继承自 BaseService class. Let’s take a look at this:

从“@angular/core”中导入{Injectable};
从“@angular/common/http”中导入{HttpClient, HttpHeaders};
从'rxjs/Observable'中导入{Observable};
从'rxjs/observable/of'中导入{of};
从rxjs/operators中导入{catchError, map, tap};
import { Helpers } from '../helpers/helpers';
@Injectable()
export class BaseService {
    构造函数(私有helper: Helpers) {}
    公共extractData(res: Response) {
        let body = res.json();
        return body || {};
      }
      public handleError(error: Response | any) {
        //在现实世界的应用中,我们可能会使用远程日志基础设施
        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.附加('Authorization', 'Bearer ' + this.helper.getToken()); 
        }
        return { headers: header };
      }
      public setToken(data:any) {
        this.helper.setToken(data);
      }
      public failToken(错误:Response | any) {
        this.helper.failToken();
        return this.handleError(Response);
      }
 }

所以每次我们进行HTTP调用时,我们实现请求的报头 super.header. If the token is in localStorage 然后它将被添加到header中,但如果没有,我们将只设置JSON格式. 这里我们可以看到的另一件事是,如果身份验证失败会发生什么.

登录组件将调用服务类,服务类将调用后端. Once we have the token, helper类将管理令牌, 现在我们准备从数据库中获取用户列表.

To get data from the database, 首先,确保模型类与响应中的后端视图模型相匹配.

In user.ts:

export class User {
  id: number;
  name: string;
}

And we can create now the user.service.ts file:

从“@angular/core”中导入{Injectable};
从“@angular/common/http”中导入{HttpClient, HttpHeaders};
从'rxjs/Observable'中导入{Observable};
从'rxjs/observable/of'中导入{of};
从rxjs/operators中导入{catchError, map, tap};
import { BaseService } from './base.service';
import { User } from '../models/user';
import { AppConfig } from '../config/config';
import { Helpers } from '../helpers/helpers';
@Injectable()
导出类UserService扩展BaseService {
  private pathAPI = this.config.setting['PathAPI'];
  constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); }
  /**从服务器获取英雄列表*/
  getUsers (): Observable {
    return this.http.get(this.pathAPI + 'user', super.header()).pipe(
    catchError(super.handleError));
  }

The Back End

Quick Start

欢迎来到Web API Core 2应用程序的第一步. 我们需要做的第一件事是创建一个ASP.. NET Core Web应用程序,我们将其称为 SeedAPI.Web.API.

Creating a new file

一定要选择Empty模板来开始一个干净的开始,如下图所示:

choose the Empty template

这就是全部,我们从一个空的web应用程序开始创建解决方案. 现在我们的架构将如下所示,因此我们必须创建不同的项目:

our current architecture

要做到这一点,只需右键单击解决方案并添加“类库(.NET Core)” project.

add a "Class Library (.NET Core)"

The Architecture

在前一节中,我们创建了8个项目,但它们的用途是什么呢? 以下是对每一个的简单描述:

  • Web.API:这是我们的启动项目,也是创建端点的地方. 这里我们将设置JWT、注入依赖项和控制器.
  • ViewModels在这里,我们执行从控制器将在响应中返回到前端的数据类型的转换. 将这些类与前端模型相匹配是一种很好的做法.
  • Interfaces这将有助于实现注入依赖. 静态类型语言引人注目的好处是,编译器可以帮助验证代码所依赖的契约是否确实得到满足.
  • Commons:所有共享的行为和实用程序代码都在这里.
  • Models:最好不要将数据库直接与前端匹配 ViewModels, so the purpose of Models 是否创建独立于前端的实体数据库类. 这将允许我们在未来改变我们的数据库,而不必对我们的前端产生影响. 当我们只想做一些重构时,它也会有所帮助.
  • Maps: Here is where we map ViewModels to Models and vice-versa. 这个步骤在控制器和服务之间被调用.
  • Services:存储所有业务逻辑的库.
  • Repositories这是我们调用数据库的唯一地方.

引用将看起来像这样:

Diagram of references

JWT-based Authentication

In this section, 我们将看到令牌身份验证的基本配置,并更深入地讨论安全性问题.

要开始设置JSON web令牌(JWT),让我们在类中创建下一个类 App_Start folder called JwtTokenConfig.cs. 里面的代码看起来像这样:

namespace SeedAPI.Web.API.App_Start
{
    public class JwtTokenConfig
    {
        添加认证(IServiceCollection services, IConfiguration configuration)
        {
            services.AddAuthentication (JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters =新的TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true;
                    ValidIssuer = configuration["Jwt:Issuer"],
                    ValidAudience = configuration["Jwt:Issuer"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(配置(“Jwt:关键”)))
                };
                services.AddCors();
            });
        }
    }
}

验证参数的值将取决于每个项目的需求. 我们可以设置读取配置文件的有效用户和受众 appsettings.json:

"Jwt": {
    "Key": "veryVerySecretKey",
    “发行人”:“http://localhost: 50498 /”
  }

那么我们只需要从 ConfigureServices method in startup.cs:

 //该方法由运行时调用. 使用此方法将服务添加到容器中.
        ConfigureServices(IServiceCollection services)
        {
            DependencyInjectionConfig.AddScope(services);
            JwtTokenConfig.AddAuthentication(服务、配置);
            DBContextConfig.初始化(服务、配置);
            services.AddMvc();
        }

现在我们准备创建第一个控制器 TokenController.cs. The value we set in appsettings.json to "veryVerySecretKey" 应该与我们用来创建令牌的匹配,但是首先,让我们创建 LoginViewModel inside our ViewModels project:

namespace SeedAPI.ViewModels
{
    公共类LoginViewModel: IBaseViewModel
    {
        public string username { get; set; }
        public string password { get; set; }
    }
}

And finally the controller:

namespace SeedAPI.Web.API.Controllers
{
    [Route("api/Token")]
    公共类TokenController:控制器
    {
        private configuration_config;
        公共TokenController(IConfiguration配置)
        {
            _config = config;
        }
        [AllowAnonymous]
        [HttpPost]
        public dynamic Post([FromBody]LoginViewModel login)
        {
            IActionResult response = Unauthorized();
            var user = Authenticate(login);
            if (user != null)
            {
                var tokenString = BuildToken(用户);
                response = Ok(new {token = tokenString});
            }
            return response;
        }
        私有字符串BuildToken(UserViewModel用户)
        {
            var key = new SymmetricSecurityKey(编码.UTF8.GetBytes(_config["Jwt:Key"]));
            var creds =新签名凭证(密钥,安全算法.HmacSha256);
            var token = new JwtSecurityToken(_config["Jwt:Issuer"],
              _config["Jwt:Issuer"],
              expires: DateTime.Now.AddMinutes(30),
              signingCredentials: creds);
            返回新的JwtSecurityTokenHandler().WriteToken(token);
        }
        私有UserViewModel认证(LoginViewModel登录)
        {
            UserViewModel user = null;
            if (login.username == "pablo" && login.password == "secret")
            {
                user = new UserViewModel {name = "Pablo"};
            }
            return user;
        }
    }
}

The BuildToken 方法将使用给定的安全代码创建令牌. The Authenticate 方法目前只硬编码了用户验证, 但是我们最终需要调用数据库来验证它.

The Application Context

自从微软推出Core 2以来,设置实体框架变得非常容易.0 version—EF Core 2 for short. 我们将深入研究代码优先模型 identityDbContext,所以首先要确保你已经安装了所有的依赖项. 你可以使用NuGet来管理它:

Getting dependencies

Using the Models 项目,我们可以在 Context folder two files, ApplicationContext.cs and IApplicationContext.cs. Also, we will need an EntityBase class.

Classes

The EntityBase 文件将由每个实体模型继承,但是 User.cs 是一个身份类和唯一的实体,将继承从 IdentityUser. Below are both classes:

namespace SeedAPI.Models
{
    公共类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;
        }
        公共虚拟对象[]IdentityID(bool dummy = true)
        {
            return new List().ToArray();
        }
    }
}


Now we are ready to create ApplicationContext.cs, which will look like this:

namespace SeedAPI.Models.Context
{
    public class ApplicationContext : IdentityDbContext, IApplicationContext
    {
        私有IDbContextTransaction dbContextTransaction
        public ApplicationContext(dbcontextopoptions)
            : base(options)
        {
                   }
        public DbSet UsersDB { get; set; }
        public new void SaveChanges()
        {
            base.SaveChanges();
        }
        public new DbSet Set() where T : class
        {
            return base.Set();
        }
        public void BeginTransaction()
        {
            dbContextTransaction =数据库.BeginTransaction();
        }
        公共void CommitTransaction()
        {
            if (dbContextTransaction != null)
            {
                dbContextTransaction.Commit();
            }
        }
        RollbackTransaction()
        {
            if (dbContextTransaction != null)
            {
                dbContextTransaction.Rollback();
            }
        }
        DisposeTransaction()
        {
            if (dbContextTransaction != null)
            {
                dbContextTransaction.Dispose();
            }
        }
    }
}

我们真的很接近了,但首先我们需要创建更多的类,这次是在 App_Start folder located in the Web.API project. 第一个类用于初始化应用程序上下文,第二个类用于创建示例数据,以便在开发期间进行测试.

namespace SeedAPI.Web.API.App_Start
{
    public class DBContextConfig
    {
        初始化(IConfiguration)配置, 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))作为IUserMap;
                new DBInitializeConfig(service).DataTest();
            }
        }
        初始化(IServiceCollection services, IConfiguration configuration)
        {
            services.AddDbContext(options =>
              options.UseSqlServer(configuration.GetConnectionString(“DefaultConnection”)));
        }
    }
}
namespace SeedAPI.Web.API.App_Start
{
    公共类DBInitializeConfig
    {
        private IUserMap userMap;
        DBInitializeConfig (IUserMap _userMap)
        {
            userMap = _userMap;
        }
        public void DataTest()
        {
            Users();
        }
        private void Users()
        {
            userMap.创建(new UserViewModel() {id = 1, name = "Pablo"});
            userMap.创建(new UserViewModel() {id = 2, name = "Diego"});
        }
    }
}

我们从启动文件中调用它们:

 //该方法由运行时调用. 使用此方法将服务添加到容器中.
        ConfigureServices(IServiceCollection services)
        {
            DependencyInjectionConfig.AddScope(services);
            JwtTokenConfig.AddAuthentication(服务、配置);
            DBContextConfig.初始化(服务、配置);
            services.AddMvc();
        }
// ...
 //该方法由运行时调用. 使用此方法配置HTTP请求管道.
        配置(IApplicationBuilder应用程序,IHostingEnvironment环境,IServiceProvider svp)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            DBContextConfig.初始化(配置,env, svp);
            app.UseCors(builder => builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader()
                .AllowCredentials());
            app.UseAuthentication();
            app.UseMvc();
        }

Dependency Injection

使用依赖注入在不同的项目之间移动是一个很好的实践. 这将帮助我们在控制器和映射器之间进行通信, 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
{
    公共类DependencyInjectionConfig
    {
        AddScope(IServiceCollection services)
        {
            services.AddScoped();
            services.AddScoped();
            services.AddScoped();
            services.AddScoped();
        }
    }
}

image alt text

我们需要为每个新实体创建一个新的 Map, Service, and Repository, and match them to this file. 然后我们只需要从 startup.cs file:

//该方法由运行时调用. 使用此方法将服务添加到容器中.
        ConfigureServices(IServiceCollection services)
        {
            DependencyInjectionConfig.AddScope(services);
            JwtTokenConfig.AddAuthentication(服务、配置);
            DBContextConfig.初始化(服务、配置);
            services.AddMvc();
        }

Finally, 当我们需要从数据库中获取用户列表时, 我们可以使用依赖注入来创建一个控制器:

namespace SeedAPI.Web.API.Controllers
{
    [Route("api/[controller]")]
    [Authorize]
    公共类UserController: Controller
    {
        IUserMap userMap;
        公共UserController(IUserMap映射)
        {
            userMap = map;
        }
        // GET api/user
        [HttpGet]
        public IEnumerable 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 属性,以确保前端已经登录,以及依赖注入如何在类的构造函数中工作.

我们最终调用了数据库,但首先,我们需要理解 Map project.

The Maps Project

This step is just to map ViewModels to and from database models. 我们必须为每个实体创建一个,并按照前面的示例创建 UserMap.cs file will look like this:

namespace SeedAPI.Maps
{
    公共类UserMap: IUserMap
    {
        IUserService userService;
        公共UserMap(IUserService服务)
        {
            userService = service;
        }
        创建UserViewModel视图模型
        {
            用户User = ViewModelToDomain(viewModel);
            返回DomainToViewModel (userService.Create(user));
        }
        public bool Update(UserViewModel)
        {
            用户User = ViewModelToDomain(viewModel);
            return userService.Update(user);
        }
        public bool Delete(int id)
        {
            return userService.Delete(id);
        }
        public List GetAll()
        {
            返回DomainToViewModel (userService.GetAll());
        }
        public UserViewModel DomainToViewModel(用户域)
        {
            UserViewModel model = new UserViewModel();
            model.name = domain.Name;
            return model;
        }
        public List DomainToViewModel(List domain)
        {
            List model = new List();
            foreach (User of in domain)
            {
                model.Add(DomainToViewModel(of));
            }
            return model;
        }
        公共用户ViewModelToDomain(UserViewModel officeViewModel)
        {
            User domain = new User();
            domain.Name = officeViewModel.name;
            return domain;
        }
    }
}

Looks like once more, 依赖注入在类的构造函数中工作, 将Maps链接到Services项目.

The Services Project

这里没有太多要说的:我们的示例非常简单,这里不需要编写业务逻辑或代码. 当我们需要在数据库或控制器步骤之前或之后计算或执行一些逻辑时,该项目将在未来的高级需求中证明是有用的. 下面的例子中,这个类看起来非常简单:

namespace SeedAPI.Services
{
    公共类UserService: IUserService
    {
        private IUserRepository存储库;
        public UserService(IUserRepository)
        {
            repository = userRepository;
        }
        public创建用户(用户域)
        {
            return repository.Save(domain);
        }
        public bool更新(用户域)
        {
            return repository.Update(domain);
        }
        public bool Delete(int id)
        {
            return repository.Delete(id);
        }
        public List GetAll()
        {
            return repository.GetAll();
        }
    }
}

The Repositories Project

我们将进入本教程的最后一部分:我们只需要调用数据库, so we create a UserRepository.cs 文件,我们可以在其中读取、插入或更新数据库中的用户.

namespace SeedAPI.Repositories
{
    公共类UserRepository: BaseRepository, IUserRepository
    {
        public UserRepository(IApplicationContext)
            : base(context)
        { }
        public User Save(User domain)
        {
            try
            {
                var us = InsertUser(domain);
                return us;
            }
            catch (Exception ex)
            {
                //ErrorManager.ErrorHandler.HandleError(ex);
                throw ex;
            }
        }
        public bool更新(用户域)
        {
            try
            {
                //domain.Updated = DateTime.Now;
                UpdateUser(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);
                    return true;
                }
                else
                {
                    return false;
                }
            }
            catch (Exception ex)
            {
                //ErrorManager.ErrorHandler.HandleError(ex);
                throw ex;
            }
        }
        public List GetAll()
        {
            try
            {
                return Context.UsersDB.OrderBy(x => x.Name).ToList();
            }
            catch (Exception ex)
            {
                //ErrorManager.ErrorHandler.HandleError(ex);
                throw ex;
            }
        }
    }
}

Summary

在这篇文章中,我解释了如何使用Angular 5和Web API Core 2创建一个好的架构. At this point, 您已经为一个大型项目创建了基础,该项目的代码支持需求的大量增长.

The truth is, 在前端没有什么能与JavaScript竞争,如果你在后端需要SQL Server和实体框架的支持,又有什么能与c#竞争呢? 所以这篇文章的想法是结合两个世界的优点,我希望你喜欢它.

What’s Next for Angular With .NET Core?

如果你在一个团队中工作 Angular developers 可能会有不同的开发人员在前端和后端工作, 因此,将Swagger与Web API 2集成是同步两个团队工作的一个好主意. Swagger是一个记录和测试RESTFul api的好工具. Read the Microsoft guide: 开始使用Swashbuckle和ASP.NET Core.

如果你对Angular 5还很陌生,并且在跟上本文时遇到了困难,请阅读 Angular 5教程:一步一步教你的第一个Angular 5应用 谢尔盖·莫伊谢耶夫的作品. 虽然它不是一个真正的Angular c#教程, 如果你需要温习Angular/ c#的基本技能,你也可以查看微软的文档.

Understanding the basics

  • 应该使用Visual Studio Code还是Microsoft Visual Studio进行前端编辑?

    您可以使用任何一个IDE作为前端, 许多人喜欢将前端加入Web应用程序库并自动部署. 我更喜欢将前端代码与后端代码分开,并发现Visual Studio code是一个非常好的工具, especially with intellisense, for Typescript code.

  • 什么是Angular材质设计,我们是否应该使用它?

    Angular Material是一个UI组件框架,虽然你不需要使用它. UI组件框架帮助我们在网站上组织布局和响应,但我们在市场上有很多这样的框架,比如bootstrap和其他,我们可以选择我们喜欢的外观和感觉.

  • 什么是Angular的路由和导航?

    当用户执行应用任务时,Angular的路由器支持从一个视图导航到下一个视图. 它可以将浏览器URL解释为导航到客户端生成的视图的指令. 允许开发人员设置URL名称, 参数并使用CanAuthenticate,我们可以验证用户的authentica

  • 依赖注入在c#中的用途是什么?

    类通常需要相互访问, 这个设计模式演示了如何创建松耦合类. 当两个类紧密耦合时,它们通过二进制关联连接在一起.

  • 什么是基于jwt的身份验证?

    JSON Web令牌(JWT)是一个紧凑的库,用于作为JSON对象在各方之间安全地传输信息. jwt也可以加密,以提供各方之间的保密,我们将重点关注签名令牌.

聘请Toptal这方面的专家.
Hire Now
Pablo Albella's profile image
Pablo Albella

Located in Málaga, Spain

Member since June 23, 2014

About the author

Pablo是一个天才的JavaScript和 .NET developer. 在过去的十年里,他成功地创造了许多复杂的产品.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Previously At

Globant

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Join the Toptal® community.