Angular5とASP.NETCore
公開: 2022-03-11Angularの最初のバージョンがクライアント側で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
を実行します。
dist
という名前の新しいフォルダーが作成され、IISまたは任意のWebサーバーを使用して提供できます。 次に、ブラウザにURLを入力して、…完了です。
ノート
このチュートリアルの目的はWebサーバーのセットアップ方法を示すことではないので、あなたはすでにその知識を持っていると思います。 |
src
フォルダー
私のsrc
フォルダーは次のように構成されていapp
フォルダー内には、Angularコンポーネントごとにcss
、 ts
、 spec
、およびhtml
ファイルを作成するcomponents
があります。 また、サイト構成を保持するためのconfig
フォルダーを作成します。 directives
にはすべてのカスタムディレクティブが含まれ、 helpers
には認証マネージャーなどの共通コードが格納され、 layout
には本体、ヘッド、サイドパネルなどの主要コンポーネントが含まれ、 models
はバックエンドビューモデルと一致し、最終的にservices
はバックエンドへのすべての呼び出しのコードを持ちます。
app
フォルダーの外では、 assets
やenvironments
などのデフォルトで作成されたフォルダーと、ルートファイルを保持します。
構成ファイルの作成
config
フォルダー内にconfig.ts
ファイルを作成し、クラスAppConfig
を呼び出します。 ここで、コードのさまざまな場所で使用するすべての値を設定できます。 たとえば、APIのURL。 このクラスは、パラメーターとしてキー/値構造と同じ値へのアクセスを取得するための単純なメソッドを受け取るget
プロパティを実装していることに注意してください。 このようにして、それを継承するクラスからthis.config.setting['PathAPI']
を呼び出すだけで値を簡単に取得できます。
import { Injectable } from '@angular/core'; @Injectable() export class AppConfig { private _config: { [key: string]: string }; constructor() { this._config = { PathAPI: 'http://localhost:50498/api/' }; } get setting():{ [key: string]: string } { return this._config; } get(key: any) { return this._config[key]; } };
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
からRouterModule
とRoutes
をインポートするだけで、実装するパスをマッピングできます。 ここでは、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)」プロジェクトを追加します。
建築学、建築物、建築様式
前のセクションで8つのプロジェクトを作成しましたが、それらは何のためにありますか? それぞれの簡単な説明は次のとおりです。
-
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
メソッドには、現時点ではユーザー検証がハードコーディングされていますが、最終的にはデータベースを呼び出して検証する必要があります。
アプリケーションコンテキスト
MicrosoftがCore2.0バージョン(略してEF Core 2 )をリリースして以来、EntityFrameworkのセットアップは非常に簡単です。 identityDbContext
を使用したコードファーストモデルについて詳しく説明します。最初に、すべての依存関係がインストールされていることを確認してください。 NuGetを使用して管理できます。
Models
プロジェクトを使用して、ここでContext
フォルダー内にApplicationContext.cs
とIApplicationContext.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アプリのステップバイステップガイドをお読みください。