Sudut 5 dan ASP.NET Core

Diterbitkan: 2022-03-11

Saya telah berpikir untuk menulis posting blog sejak versi pertama Angular praktis membunuh Microsoft di sisi klien. Teknologi seperti ASP.Net, Web Forms, dan MVC Razor telah menjadi usang, digantikan oleh kerangka JavaScript yang sebenarnya bukan Microsoft. Namun, sejak Angular versi kedua, Microsoft dan Google telah bekerja sama untuk membuat Angular 2, dan inilah saat dua teknologi favorit saya mulai bekerja sama.

Di blog ini, saya ingin membantu orang-orang membuat arsitektur terbaik yang menggabungkan dua dunia ini. Apakah kamu siap? Ini dia!

Tentang arsitektur

Anda akan membangun klien Angular 5 yang menggunakan layanan RESTful Web API Core 2.

Sisi klien:

  • Sudut 5
  • CLI sudut
  • Bahan Sudut

Sisi server:

  • .NET C# Web API Core 2
  • Ketergantungan injeksi
  • Otentikasi JWT
  • Kode kerangka kerja entitas terlebih dahulu
  • SQL Server

Catatan

Dalam posting blog ini kami mengasumsikan pembaca sudah memiliki pengetahuan dasar tentang TypeScript, modul Angular, komponen, dan mengimpor/mengekspor. Tujuan dari posting ini adalah untuk membuat arsitektur yang baik yang memungkinkan kode untuk berkembang dari waktu ke waktu.

Apa yang kamu butuhkan?

Mari kita mulai dengan memilih IDE. Tentu saja, ini hanya preferensi saya, dan Anda dapat menggunakan yang Anda rasa lebih nyaman. Dalam kasus saya, saya akan menggunakan Visual Studio Code dan Visual Studio 2017.

Mengapa dua IDE yang berbeda? Sejak Microsoft membuat Visual Studio Code untuk front end, saya tidak bisa berhenti menggunakan IDE ini. Bagaimanapun, kita juga akan melihat bagaimana mengintegrasikan Angular 5 di dalam proyek solusi, yang akan membantu Anda jika Anda adalah tipe pengembang yang lebih suka men-debug kedua ujung belakang dan depan hanya dengan satu F5.

Soal back end, Anda bisa menginstal versi terbaru Visual Studio 2017 yang memiliki edisi gratis untuk developer namun sangat lengkap: Community.

Jadi, inilah daftar hal-hal yang perlu kita instal untuk tutorial ini:

  • Kode Visual Studio
  • Komunitas Visual Studio 2017 (atau Apa Saja)
  • Node.js v8.10.0
  • SQL Server 2017

Catatan

Pastikan Anda menjalankan setidaknya Node 6.9.x dan npm 3.xx dengan menjalankan node -v dan npm -v di terminal atau jendela konsol. Versi yang lebih lama menghasilkan kesalahan, tetapi versi yang lebih baru baik-baik saja.

Bagian Depan

Mulai cepat

Biarkan kesenangan dimulai! Hal pertama yang perlu kita lakukan adalah menginstal Angular CLI secara global, jadi buka command prompt node.js dan jalankan perintah ini:

 npm install -g @angular/cli

Oke, sekarang kita memiliki bundler modul kita. Ini biasanya menginstal modul di bawah folder pengguna Anda. Alias ​​​​seharusnya tidak diperlukan secara default, tetapi jika Anda membutuhkannya, Anda dapat menjalankan baris berikutnya:

 alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"

Langkah selanjutnya adalah membuat proyek baru. Saya akan menyebutnya angular5-app . Pertama, kami menavigasi ke folder tempat kami ingin membuat situs, lalu:

 ng new angular5-app

Bangun Pertama

Meskipun Anda dapat menguji situs web baru Anda hanya dengan menjalankan ng serve --open , saya sarankan untuk menguji situs tersebut dari layanan web favorit Anda. Mengapa? Nah, beberapa masalah hanya dapat terjadi dalam produksi, dan membangun situs dengan ng build adalah cara terdekat untuk mendekati lingkungan ini. Kemudian kita dapat membuka folder angular5-app dengan Visual Studio Code dan menjalankan ng build di terminal bash:

membangun aplikasi sudut untuk pertama kalinya

Folder baru bernama dist akan dibuat dan kami dapat menyajikannya menggunakan IIS atau server web mana pun yang Anda inginkan. Kemudian Anda dapat mengetikkan URL di browser, dan…selesai!

struktur direktori baru

Catatan

Bukan tujuan dari tutorial ini untuk menunjukkan cara mengatur server web, jadi saya berasumsi Anda sudah memiliki pengetahuan itu.

Layar Selamat Datang Sudut 5

Folder src

Struktur folder src

Folder src saya disusun sebagai berikut: Di dalam folder app kami memiliki components di mana kami akan membuat untuk setiap komponen Angular file css , ts , spec , dan html . Kami juga akan membuat folder config untuk menyimpan konfigurasi situs, directives akan memiliki semua arahan khusus kami, helpers akan menampung kode umum seperti manajer otentikasi, layout akan berisi komponen utama seperti bodi, kepala, dan panel samping, models menyimpan apa yang akan cocok dengan model tampilan back-end, dan akhirnya services akan memiliki kode untuk semua panggilan ke back-end.

Di luar folder app , kami akan menyimpan folder yang dibuat secara default, seperti assets dan environments , dan juga file root.

Membuat File Konfigurasi

Mari buat file config.ts di dalam folder config kita dan panggil kelas AppConfig . Di sinilah kita dapat mengatur semua nilai yang akan kita gunakan di tempat yang berbeda dalam kode kita; misalnya, URL API. Perhatikan bahwa kelas mengimplementasikan properti get yang menerima, sebagai parameter, struktur kunci/nilai dan metode sederhana untuk mendapatkan akses ke nilai yang sama. Dengan cara ini, akan mudah untuk mendapatkan nilai hanya dengan memanggil this.config.setting['PathAPI'] dari kelas yang mewarisinya.

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

Bahan Sudut

Sebelum memulai tata letak, mari siapkan kerangka kerja komponen UI. Tentu saja, Anda dapat menggunakan yang lain seperti Bootstrap, tetapi jika Anda menyukai gaya Material, saya merekomendasikannya karena itu juga didukung oleh Google.

Untuk menginstalnya, kita hanya perlu menjalankan tiga perintah berikutnya, yang dapat kita jalankan di terminal Visual Studio Code:

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

Perintah kedua adalah karena beberapa komponen Material bergantung pada Angular Animations. Saya juga merekomendasikan membaca halaman resmi untuk memahami browser mana yang didukung dan apa itu polyfill.

Perintah ketiga adalah karena beberapa komponen Material mengandalkan HammerJS untuk gerakan.

Sekarang kita dapat melanjutkan untuk mengimpor modul komponen yang ingin kita gunakan dalam file app.module.ts kita:

 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 ],

Langkah selanjutnya adalah mengubah file style.css , menambahkan jenis tema yang ingin Anda gunakan:

 @import "~@angular/material/prebuilt-themes/deeppurple-amber.css";

Sekarang impor HammerJS dengan menambahkan baris ini di file main.ts :

 import 'hammerjs';

Dan akhirnya semua yang kita lewatkan adalah menambahkan ikon Material ke index.html , di dalam bagian kepala:

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

Tata Letak

Dalam contoh ini, kita akan membuat layout sederhana seperti ini:

Contoh tata letak

Idenya adalah untuk membuka/menyembunyikan menu dengan mengklik beberapa tombol di header. Angular Responsive akan melakukan sisanya untuk kita. Untuk melakukan ini, kita akan membuat folder layout dan memasukkan ke dalamnya file app.component yang dibuat secara default. Tetapi kami juga akan membuat file yang sama untuk setiap bagian tata letak seperti yang dapat Anda lihat pada gambar berikutnya. Kemudian, app.component akan menjadi menu body, head.component header, dan left-panel.component .

Folder konfigurasi yang disorot

Sekarang mari kita ubah app.component.html sebagai berikut:

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

Pada dasarnya kita akan memiliki properti authentication dalam komponen yang memungkinkan kita untuk menghapus header dan menu jika pengguna tidak login, dan sebagai gantinya, menampilkan halaman login sederhana.

head.component.html terlihat seperti ini:

 <h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>

Hanya sebuah tombol untuk mengeluarkan pengguna—kami akan kembali lagi nanti. Sedangkan untuk left-panel.component.html , untuk saat ini ubah saja HTMLnya menjadi:

 <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>

Kami membuatnya tetap sederhana: Sejauh ini hanya ada dua tautan untuk menavigasi melalui dua halaman yang berbeda. (Kami juga akan kembali ke ini nanti.)

Sekarang, seperti inilah tampilan kepala dan komponen sisi kiri file 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'; }

Tapi bagaimana dengan kode TypeScript untuk app.component ? Kami akan meninggalkan sedikit misteri di sini dan menghentikannya sebentar, dan kembali ke sini setelah menerapkan otentikasi.

Rute

Oke, sekarang kami memiliki Angular Material yang membantu kami dengan UI dan tata letak sederhana untuk mulai membangun halaman kami. Tapi bagaimana kita bisa menavigasi antar halaman?

Untuk membuat contoh sederhana, mari buat dua halaman: "Pengguna," di mana kita bisa mendapatkan daftar pengguna yang ada di database, dan "Dasbor," halaman di mana kita bisa menampilkan beberapa statistik.

Di dalam folder app kita akan membuat file bernama app-routing.modules.ts terlihat seperti ini:

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

Sesederhana itu: Hanya dengan mengimpor RouterModule dan Routes dari @angular/router , kita dapat memetakan jalur yang ingin kita terapkan. Di sini kita membuat empat jalur:

  • /dashboard : Halaman utama kami
  • /login : Halaman di mana pengguna dapat mengautentikasi
  • /logout : Jalur sederhana untuk mengeluarkan pengguna
  • /users : Halaman pertama kami di mana kami ingin membuat daftar pengguna dari bagian belakang

Perhatikan bahwa dashboard adalah halaman kami secara default, jadi jika pengguna mengetikkan URL / , halaman tersebut akan dialihkan secara otomatis ke halaman ini. Juga, lihat parameter canActivate : Di sini kita membuat referensi ke kelas AuthGuard , yang akan memungkinkan kita untuk memeriksa apakah pengguna login. Jika tidak, itu dialihkan ke halaman login. Di bagian selanjutnya, saya akan menunjukkan cara membuat kelas ini.

Sekarang, yang perlu kita lakukan adalah membuat menu. Ingat di bagian layout saat kita membuat file left-panel.component.html agar terlihat seperti ini?

 <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>

Di sinilah kode kita bertemu dengan kenyataan. Sekarang kita dapat membuat kode dan mengujinya di URL: Anda seharusnya dapat menavigasi dari halaman Dasbor ke Pengguna, tetapi apa yang terjadi jika Anda mengetikkan URL our.site.url/users di browser secara langsung?

teks alternatif gambar

Perhatikan bahwa kesalahan ini juga muncul jika Anda me-refresh browser setelah berhasil menavigasi ke URL tersebut melalui panel samping aplikasi. Untuk memahami kesalahan ini, izinkan saya merujuk ke dokumen resmi yang sangat jelas:

Aplikasi yang dirutekan harus mendukung tautan dalam. Tautan dalam adalah URL yang menentukan jalur ke komponen di dalam aplikasi. Misalnya, http://www.mysite.com/users/42 adalah tautan dalam ke halaman detail pahlawan yang menampilkan pahlawan dengan id: 42.

Tidak ada masalah saat pengguna menavigasi ke URL itu dari dalam klien yang sedang berjalan. Router Angular menafsirkan URL dan merutekan ke halaman dan pahlawan itu.

Tetapi mengeklik tautan di email, memasukkannya di bilah alamat peramban, atau sekadar menyegarkan peramban saat berada di halaman detail pahlawan — semua tindakan ini ditangani oleh peramban itu sendiri, di luar aplikasi yang sedang berjalan. Browser membuat permintaan langsung ke server untuk URL itu, melewati router.

Server statis secara rutin mengembalikan index.html ketika menerima permintaan untuk http://www.mysite.com/ . Tapi itu menolak http://www.mysite.com/users/42 dan mengembalikan kesalahan 404 - Not Found kecuali jika dikonfigurasi untuk mengembalikan index.html sebagai gantinya.

Untuk memperbaiki masalah ini sangat sederhana, kita hanya perlu membuat konfigurasi file penyedia layanan. Karena saya bekerja dengan IIS di sini, saya akan menunjukkan kepada Anda bagaimana melakukannya di lingkungan ini, tetapi konsepnya serupa untuk Apache atau server web lainnya.

Jadi kami membuat file di dalam folder src bernama web.config yang terlihat seperti ini:

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

Maka kita perlu memastikan bahwa aset ini akan disalin ke folder yang digunakan. Yang perlu kita lakukan adalah mengubah file pengaturan Angular CLI kita 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": {} } }

Autentikasi

Apakah Anda ingat bagaimana kami mengimplementasikan kelas AuthGuard untuk mengatur konfigurasi perutean? Setiap kali kami menavigasi ke halaman yang berbeda, kami akan menggunakan kelas ini untuk memverifikasi apakah pengguna diautentikasi dengan token. Jika tidak, kami akan mengarahkan ulang secara otomatis ke halaman login. File untuk ini adalah canActivateAuthGuard.ts di dalam folder helpers dan buat seperti ini:

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

Jadi setiap kali kami mengubah halaman, metode canActivate akan dipanggil, yang akan memeriksa apakah pengguna diautentikasi, dan jika tidak, kami menggunakan instance Router kami untuk mengarahkan ulang ke halaman login. Tapi apa metode baru ini di kelas Helper ? Di bawah folder helpers mari kita buat file helpers.ts . Disini kita perlu mengelola localStorage , dimana kita akan menyimpan token yang kita dapatkan dari back end.

Catatan

Mengenai localStorage , Anda juga dapat menggunakan cookie atau sessionStorage , dan keputusannya akan bergantung pada perilaku yang ingin kita terapkan. Seperti namanya, sessionStorage hanya tersedia selama sesi browser, dan dihapus saat tab atau jendela ditutup; itu, bagaimanapun, bertahan dari pemuatan ulang halaman. Jika data yang Anda simpan harus tersedia secara berkelanjutan, maka localStorage lebih disukai daripada sessionStorage . Cookie terutama untuk membaca sisi server, sedangkan localStorage hanya dapat dibaca sisi klien. Jadi pertanyaannya adalah, di aplikasi Anda, siapa yang membutuhkan data ini---klien atau server?


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

Apakah kode otentikasi kami masuk akal sekarang? Kita akan kembali ke kelas Subject nanti, tapi sekarang mari kita kembali ke konfigurasi perutean sebentar. Lihatlah baris ini:

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

Ini adalah komponen kami untuk keluar dari situs, dan ini hanya kelas sederhana untuk membersihkan localStorage . Mari kita buat di bawah folder components/login dengan nama 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']); } }

Jadi setiap kali kita membuka URL /logout , localStorage akan dihapus dan situs akan dialihkan ke halaman login. Terakhir, mari kita buat login.component.ts seperti ini:

 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']); }); } }

Seperti yang Anda lihat, untuk saat ini kami telah mengkodekan kredensial kami di sini. Perhatikan bahwa di sini kita memanggil kelas layanan; kami akan membuat kelas layanan ini untuk mendapatkan akses ke bagian belakang kami di bagian selanjutnya.

Terakhir, kita perlu kembali ke file app.component.ts , tata letak situs. Di sini, jika pengguna diautentikasi, itu akan menampilkan bagian menu dan header, tetapi jika tidak, tata letak akan berubah untuk hanya menampilkan halaman login kami.

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

Ingat kelas Subject di kelas pembantu kita? Ini adalah Observable . Observable s memberikan dukungan untuk menyampaikan pesan antara penerbit dan pelanggan di aplikasi Anda. Setiap kali token otentikasi berubah, properti authentication akan diperbarui. Meninjau file app.component.html , mungkin akan lebih masuk akal sekarang:

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

Jasa

Pada titik ini kami menavigasi ke halaman yang berbeda, mengautentikasi sisi klien kami, dan membuat tata letak yang sangat sederhana. Tapi bagaimana kita bisa mendapatkan data dari back end? Saya sangat merekomendasikan melakukan semua akses back-end dari kelas layanan pada khususnya. Layanan pertama kami akan berada di dalam folder services , yang disebut token.service.ts :

 import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { AppConfig } from '../config/config'; import { BaseService } from './base.service'; import { Token } from '../models/token'; import { Helpers } from '../helpers/helpers'; @Injectable() export class TokenService extends BaseService { private pathAPI = this.config.setting['PathAPI']; public errorMessage: string; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } auth(data: any): any { let body = JSON.stringify(data); return this.getToken(body); } private getToken (body: any): Observable<any> { return this.http.post<any>(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }

Panggilan pertama ke back end adalah panggilan POST ke API token. API token tidak memerlukan string token di header, tetapi apa yang terjadi jika kita memanggil titik akhir lain? Seperti yang Anda lihat di sini, TokenService (dan kelas layanan secara umum) mewarisi dari kelas BaseService . Mari kita lihat ini:

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

Jadi setiap kali kami melakukan panggilan HTTP, kami mengimplementasikan header permintaan hanya menggunakan super.header . Jika token ada di localStorage maka akan ditambahkan di dalam header, tetapi jika tidak, kami hanya akan mengatur format JSON. Hal lain yang dapat kita lihat di sini adalah apa yang terjadi jika otentikasi gagal.

Komponen login akan memanggil service class dan service class akan memanggil back end. Setelah kami memiliki token, kelas pembantu akan mengelola token, dan sekarang kami siap untuk mendapatkan daftar pengguna dari database kami.

Untuk mendapatkan data dari database, pertama-tama pastikan kita mencocokkan kelas model dengan model tampilan back-end dalam respons kita.

Di user.ts :

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

Dan sekarang kita dapat membuat file 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)); }

Bagian Belakang

Mulai cepat

Selamat datang di langkah pertama aplikasi Web API Core 2 kami. Hal pertama yang kita butuhkan adalah membuat Aplikasi Web Inti ASP.Net, yang akan kita sebut SeedAPI.Web.API .

Membuat file baru

Pastikan untuk memilih template Kosong untuk awal yang bersih seperti yang Anda lihat di bawah ini:

pilih template Kosong

Itu saja, kami membuat solusi dimulai dengan aplikasi web kosong. Sekarang arsitektur kita akan seperti yang kita daftar di bawah ini sehingga kita harus membuat proyek yang berbeda:

arsitektur kami saat ini

Untuk melakukan ini, untuk masing-masing cukup klik kanan Solusi dan tambahkan proyek "Perpustakaan Kelas (.NET Core)".

tambahkan "Perpustakaan Kelas (.NET Core)"

Arsitektur

Di bagian sebelumnya kami membuat delapan proyek, tetapi untuk apa mereka? Berikut adalah deskripsi sederhana dari masing-masing:

  • Web.API : Ini adalah proyek startup kami dan tempat endpoint dibuat. Di sini kita akan mengatur JWT, dependensi injeksi, dan pengontrol.
  • ViewModels : Di sini kami melakukan konversi dari jenis data yang akan dikembalikan oleh pengontrol dalam respons ke ujung depan. Ini adalah praktik yang baik untuk mencocokkan kelas-kelas ini dengan model front-end.
  • Interfaces : Ini akan membantu dalam mengimplementasikan dependensi injeksi. Manfaat menarik dari bahasa yang diketik secara statis adalah bahwa kompiler dapat membantu memverifikasi bahwa kontrak yang diandalkan oleh kode Anda benar-benar terpenuhi.
  • Commons : Semua perilaku bersama dan kode utilitas akan ada di sini.
  • Models : Merupakan praktik yang baik untuk tidak mencocokkan database secara langsung dengan ViewModels yang menghadap ke depan, jadi tujuan Models adalah untuk membuat kelas database entitas yang independen dari ujung depan. Itu akan memungkinkan kami di masa depan untuk mengubah database kami tanpa harus berdampak pada front end kami. Ini juga membantu ketika kita hanya ingin melakukan beberapa refactoring.
  • Maps : Di sinilah kita memetakan ViewModels ke Models dan sebaliknya. Langkah ini disebut antara pengontrol dan Layanan.
  • Services : Sebuah perpustakaan untuk menyimpan semua logika bisnis.
  • Repositories : Ini adalah satu-satunya tempat kita memanggil database.

Referensi akan terlihat seperti ini:

Diagram referensi

Otentikasi berbasis JWT

Di bagian ini, kita akan melihat konfigurasi dasar otentikasi token dan membahas lebih dalam tentang keamanan.

Untuk mulai mengatur token web JSON (JWT) mari kita buat kelas berikutnya di dalam folder App_Start bernama JwtTokenConfig.cs . Kode di dalamnya akan terlihat seperti ini:

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

Nilai parameter validasi akan tergantung pada kebutuhan setiap proyek. Pengguna dan audiens yang valid dapat kita atur dengan membaca file konfigurasi appsettings.json :

 "Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }

Maka kita hanya perlu memanggilnya dari metode ConfigureServices di startup.cs :

 // 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(); }

Sekarang kita siap untuk membuat controller pertama kita yang bernama TokenController.cs . Nilai yang kita tetapkan di appsettings.json ke "veryVerySecretKey" harus cocok dengan yang kita gunakan untuk membuat token, tetapi pertama-tama, mari buat LoginViewModel di dalam proyek ViewModels kita:

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

Dan akhirnya pengontrol:

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

Metode BuildToken akan membuat token dengan kode keamanan yang diberikan. Metode Authenticate hanya memiliki validasi pengguna yang di-hard-code untuk saat ini, tetapi kita perlu memanggil database untuk memvalidasinya pada akhirnya.

Konteks Aplikasi

Menyiapkan Entity Framework sangat mudah karena Microsoft meluncurkan versi Core 2.0— singkatnya EF Core 2 . Kita akan mendalami model kode-pertama menggunakan identityDbContext , jadi pertama-tama pastikan Anda telah menginstal semua dependensi. Anda dapat menggunakan NuGet untuk mengelolanya:

Mendapatkan dependensi

Menggunakan proyek Models kita dapat membuat di sini di dalam folder Context dua file, ApplicationContext.cs dan IApplicationContext.cs . Juga, kita akan membutuhkan kelas EntityBase .

Kelas

File EntityBase akan diwarisi oleh setiap model entitas, tetapi User.cs adalah kelas identitas dan satu-satunya entitas yang akan mewarisi dari IdentityUser . Di bawah ini adalah kedua kelas:

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

Sekarang kita siap untuk membuat ApplicationContext.cs , yang akan terlihat seperti ini:

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

Kami sangat dekat, tetapi pertama-tama kami perlu membuat lebih banyak kelas, kali ini di folder App_Start yang terletak di proyek Web.API . Kelas pertama adalah untuk menginisialisasi konteks aplikasi dan yang kedua adalah membuat data sampel hanya untuk tujuan pengujian selama pengembangan.

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

Injeksi Ketergantungan

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

image alt text

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.

Proyek Maps

Langkah ini hanya untuk memetakan ViewModels ke dan dari model database. Kita harus membuat satu untuk setiap entitas, dan, mengikuti contoh sebelumnya, file UserMap.cs akan terlihat seperti ini:

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

Sepertinya sekali lagi, injeksi ketergantungan bekerja di konstruktor kelas, menautkan Maps ke proyek Layanan.

Proyek Services

Tidak banyak yang bisa dikatakan di sini: Contoh kami sangat sederhana dan kami tidak memiliki logika bisnis atau kode untuk ditulis di sini. Proyek ini akan terbukti berguna dalam persyaratan lanjutan di masa mendatang ketika kita perlu menghitung atau melakukan beberapa logika sebelum atau setelah langkah-langkah database atau pengontrol. Mengikuti contoh kelas akan terlihat sangat kosong:

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

Proyek Repositories

Kita masuk ke bagian terakhir dari tutorial ini: Kita hanya perlu melakukan panggilan ke database, jadi kita membuat file UserRepository.cs di mana kita bisa membaca, menyisipkan, atau memperbarui pengguna di database.

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

Ringkasan

Pada artikel ini, saya menjelaskan cara membuat arsitektur yang baik menggunakan Angular 5 dan Web API Core 2. Pada titik ini, Anda telah membuat basis untuk proyek besar dengan kode yang mendukung pertumbuhan besar dalam persyaratan.

Sebenarnya, tidak ada yang bersaing dengan JavaScript di front end dan apa yang bisa bersaing dengan C# jika Anda memerlukan dukungan SQL Server dan Entity Framework di back end? Jadi ide artikel ini adalah untuk menggabungkan yang terbaik dari dua dunia dan saya harap Anda menikmatinya.

Apa berikutnya?

Jika Anda bekerja dalam tim pengembang Angular, mungkin ada pengembang berbeda yang bekerja di front end dan back end, jadi ide yang bagus untuk menyinkronkan upaya kedua tim dapat mengintegrasikan Swagger dengan Web API 2. Swagger is a great alat untuk mendokumentasikan dan menguji RESTFul API Anda. Baca panduan Microsoft: Memulai dengan Swashbuckle dan ASP.NET Core.

Jika Anda masih sangat baru di Angular 5 dan mengalami kesulitan mengikuti, baca Tutorial Angular 5: Panduan Langkah demi Langkah untuk Aplikasi Angular 5 Pertama Anda oleh sesama Toptaler Sergey Moiseev.