Angular5とASP.NETCore

公開: 2022-03-11

Angularの最初のバージョンがクライアント側でMicrosoftを事実上殺害して以来、私はブログ投稿を書くことを考えていました。 ASP.Net、Webフォーム、MVC Razorなどのテクノロジは廃止され、MicrosoftではないJavaScriptフレームワークに置き換えられました。 ただし、Angularの2番目のバージョン以降、MicrosoftとGoogleは協力してAngular 2を作成してきました。これが、私の2つのお気に入りのテクノロジーが協力し始めたときです。

このブログでは、人々がこれら2つの世界を組み合わせた最高のアーキテクチャを作成できるように支援したいと思います。 準備はできたか? どうぞ!

アーキテクチャについて

RESTful Web APICore2サービスを使用するAngular5クライアントを構築します。

クライアント側:

  • Angular 5
  • Angular CLI
  • Angular Material

サーバー側:

  • .NET C#WebAPIコア2
  • インジェクションの依存関係
  • JWT認証
  • 最初のエンティティフレームワークコード
  • SQLサーバー

ノート

このブログ投稿では、読者がTypeScript、Angularモジュール、コンポーネント、およびインポート/エクスポートの基本的な知識をすでに持っていることを前提としています。 この投稿の目的は、コードが時間の経過とともに成長することを可能にする優れたアーキテクチャを作成することです。

あなたは何が必要ですか?

IDEを選択することから始めましょう。 もちろん、これは私の好みであり、より快適に感じるものを使用できます。 私の場合は、VisualStudioCodeとVisualStudio2017を使用します。

なぜ2つの異なるIDEなのですか? Microsoftがフロントエンド用のVisualStudioCodeを作成したので、このIDEの使用をやめることはできません。 とにかく、ソリューションプロジェクト内にAngular 5を統合する方法も説明します。これは、バックエンドとフロントの両方を1つのF5でデバッグすることを好む開発者の場合に役立ちます。

バックエンドについては、開発者向けの無料版があり、非常に完全な最新のVisualStudio2017バージョンであるコミュニティをインストールできます。

したがって、このチュートリアルでインストールする必要があるもののリストは次のとおりです。

  • Visual Studio Code
  • Visual Studio 2017コミュニティ(または任意)
  • Node.js v8.10.0
  • SQL Server 2017

ノート

ターミナルまたはコンソールウィンドウでnode -v -vおよびnpm -v -vを実行して、少なくともNode6.9.xおよびnpm3.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サイトをテストできますが、お気に入りのWebサービスからサイトをテストすることをお勧めします。 なんで? いくつかの問題は本番環境でのみ発生する可能性があり、 ng buildことがこの環境にアプローチする最も近い方法です。 次に、Visual Studio Codeを使用してangular5-appフォルダーを開き、ターミナルbashでng buildを実行します。

初めてAngularアプリを構築する

distという名前の新しいフォルダーが作成され、IISまたは任意のWebサーバーを使用して提供できます。 次に、ブラウザにURLを入力して、…完了です。

新しいディレクトリ構造

ノート

このチュートリアルの目的はWebサーバーのセットアップ方法を示すことではないので、あなたはすでにその知識を持っていると思います。

Angular5ウェルカム画面

srcフォルダー

srcフォルダーの構造

私のsrcフォルダーは次のように構成されていappフォルダー内には、Angularコンポーネントごとにcsstsspec 、およびhtmlファイルを作成するcomponentsがあります。 また、サイト構成を保持するための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]; } };

Angular Material

レイアウトを開始する前に、UIコンポーネントフレームワークを設定しましょう。 もちろん、Bootstrapのような他のものを使用することもできますが、Materialのスタイリングが好きな場合は、Googleでもサポートされているのでお勧めします。

これをインストールするには、次の3つのコマンドを実行する必要があります。これらのコマンドは、VisualStudioCodeターミナルで実行できます。

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

2番目のコマンドは、一部のマテリアルコンポーネントがAngularアニメーションに依存しているためです。 また、公式ページを読んで、サポートされているブラウザーとポリフィルとは何かを理解することをお勧めします。

3番目のコマンドは、一部の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';

そして最後に欠けているのは、ヘッドセクション内のindex.htmlにマテリアルアイコンを追加することだけです。

 <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

レイアウト

この例では、次のような単純なレイアウトを作成します。

レイアウト例

ヘッダーのボタンをクリックしてメニューを開いたり非表示にしたりするのが目的です。 AngularResponsiveが残りの作業を行います。 これを行うには、 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>

シンプルにしています。これまでのところ、2つの異なるページをナビゲートするためのリンクは2つだけです。 (これにも後で戻ります。)

さて、これは頭と左側のコンポーネント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コードはどうですか? ここに少し謎を残してしばらく一時停止し、認証を実装した後でこれに戻ります。

ルーティング

これで、ページの作成を開始するためのUIとシンプルなレイアウトを支援するAngularMaterialができました。 しかし、どうすればページ間を移動できますか?

簡単な例を作成するために、データベース内の既存のユーザーのリストを取得できる「ユーザー」と、統計を表示できる「ダッシュボード」の2つのページを作成しましょう。

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をインポートするだけで、実装するパスをマッピングできます。 ここでは、4つのパスを作成しています。

  • /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でテストできます。ダッシュボードページからユーザーに移動できるはずですが、ブラウザーにURL 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を拒否し、代わりにindex.htmlを返すように構成されていない限り、404-NotFoundエラーを返します。

この問題を修正するのは非常に簡単です。サービスプロバイダーのファイル構成を作成するだけです。 ここではIISを使用しているので、この環境でそれを行う方法を説明しますが、概念はApacheやその他のWebサーバーでも同様です。

したがって、次のようなweb.configという名前のsrcフォルダー内にファイルを作成します。

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

次に、このアセットがデプロイされたフォルダーにコピーされることを確認する必要があります。 AngularCLI設定ファイル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はブラウザセッションの間のみ使用可能であり、タブまたはウィンドウが閉じられると削除されます。 ただし、ページのリロード後は存続します。 保存しているデータを継続的に利用できるようにする必要がある場合は、 sessionStorageよりもlocalStorageの方が適しています。 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>

サービス

この時点で、さまざまなページに移動し、クライアント側を認証して、非常に単純なレイアウトをレンダリングしています。 しかし、どうすればバックエンドからデータを取得できますか? 特にサービスクラスからのすべてのバックエンドアクセスを行うことを強くお勧めします。 最初のサービスは、 token.service.tsというservicesフォルダー内にあります。

 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形式を設定するだけです。 ここで確認できるもう1つのことは、認証が失敗した場合に何が起こるかです。

ログインコンポーネントはサービスクラスを呼び出し、サービスクラスはバックエンドを呼び出します。 トークンを取得すると、ヘルパークラスがトークンを管理し、データベースからユーザーのリストを取得する準備が整います。

データベースからデータを取得するには、まず、応答でモデルクラスをバックエンドビューモデルと一致させるようにします。

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 APICore2アプリケーションの最初のステップへようこそ。 最初に必要なのは、ASP.Net CoreWebアプリケーションを作成することです。これをSeedAPI.Web.APIと呼びます。

新しいファイルを作成する

以下に示すように、クリーンスタートのために空のテンプレートを選択してください。

空のテンプレートを選択します

以上で、空のWebアプリケーションからソリューションを作成します。 これで、アーキテクチャは以下のリストのようになるため、さまざまなプロジェクトを作成する必要があります。

現在のアーキテクチャ

これを行うには、それぞれについてソリューションを右クリックし、「クラスライブラリ(.NET Core)」プロジェクトを追加します。

「クラスライブラリ(.NET Core)」を追加します

建築学、建築物、建築様式

前のセクションで8つのプロジェクトを作成しましたが、それらは何のためにありますか? それぞれの簡単な説明は次のとおりです。

  • Web.API :これは私たちのスタートアッププロジェクトであり、エンドポイントが作成される場所です。 ここでは、JWT、インジェクションの依存関係、およびコントローラーを設定します。
  • ViewModels :ここでは、コントローラーがフロントエンドへの応答で返すデータのタイプから変換を実行します。 これらのクラスをフロントエンドモデルと一致させることをお勧めします。
  • Interfaces :これは、インジェクションの依存関係を実装するのに役立ちます。 静的に型付けされた言語の魅力的な利点は、コンパイラーが、コードが依存するコントラクトが実際に満たされていることを確認できることです。
  • Commons :すべての共有動作とユーティリティコードがここにあります。
  • Models :データベースをフロントエンドに面したViewModelsと直接一致させないことをお勧めします。したがって、 Modelsの目的は、フロントエンドから独立したエンティティデータベースクラスを作成することです。 これにより、将来、フロントエンドに影響を与えることなくデータベースを変更できるようになります。 また、単にリファクタリングを実行したい場合にも役立ちます。
  • Maps :ここで、 ViewModelsModelsにマップします。その逆も同様です。 このステップは、コントローラーとサービスの間で呼び出されます。
  • 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.csConfigureServicesメソッドから呼び出すだけです。

 // 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メソッドには、現時点ではユーザー検証がハードコーディングされていますが、最終的にはデータベースを呼び出して検証する必要があります。

アプリケーションコンテキスト

MicrosoftがCore2.0バージョン(略してEF Core 2 )をリリースして以来、EntityFrameworkのセットアップは非常に簡単です。 identityDbContextを使用したコードファーストモデルについて詳しく説明します。最初に、すべての依存関係がインストールされていることを確認してください。 NuGetを使用して管理できます。

依存関係の取得

Modelsプロジェクトを使用して、ここでContextフォルダー内にApplicationContext.csIApplicationContext.csの2つのファイルを作成できます。 また、 EntityBaseクラスが必要になります。

クラス

EntityBaseファイルは各エンティティモデルに継承されますが、 User.csはIDクラスであり、 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フォルダーにさらにクラスを作成する必要があります。 最初のクラスはアプリケーションコンテキストを初期化することであり、2番目のクラスは開発中のテストのみを目的としたサンプルデータを作成することです。

 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をデータベースモデルとの間でマッピングするためのものです。 エンティティごとに1つ作成する必要があります。前の例に従って、 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; } } }

もう一度、依存性注入がクラスのコンストラクターで機能し、マップをサービスプロジェクトにリンクしているように見えます。

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; } } } }

概要

この記事では、Angular5とWebAPI Core 2を使用して優れたアーキテクチャを作成する方法を説明しました。この時点で、要件の大幅な増加をサポートするコードを使用して、大規模なプロジェクトのベースを作成しました。

真実は、フロントエンドでJavaScriptと競合するものはなく、バックエンドでSQLServerとEntityFrameworkのサポートが必要な場合、C#と競合できるものは何でしょうか。 したがって、この記事のアイデアは、2つの世界の長所を組み合わせることであり、楽しんでいただけたと思います。

次は何ですか?

Angular開発者のチームで作業している場合、フロントエンドとバックエンドで異なる開発者が作業している可能性があるため、両方のチームの取り組みを同期させることをお勧めします。SwaggerをWebAPI2と統合することをお勧めします。Swaggerは素晴らしいですRESTFulAPIを文書化してテストするためのツール。 Microsoftガイドを読む:SwashbuckleとASP.NETCoreの使用を開始します。

Angular 5をまだ初めて使用していて、フォローするのに問題がある場合は、仲間のToptaler SergeyMoiseevによるAngular5チュートリアル:最初のAngular5アプリのステップバイステップガイドをお読みください。