Угловой 5 и ядро ASP.NET
Опубликовано: 2022-03-11Я думал о написании сообщения в блоге, так как первая версия Angular практически убила Microsoft на стороне клиента. Такие технологии, как ASP.Net, Web Forms и MVC Razor, устарели, их заменила среда JavaScript, которая не совсем принадлежит Microsoft. Однако, начиная со второй версии Angular, Microsoft и Google работали вместе над созданием Angular 2, и именно тогда две мои любимые технологии начали работать вместе.
В этом блоге я хочу помочь людям создать лучшую архитектуру, сочетающую эти два мира. Вы готовы? Вот так!
Об архитектуре
Вы создадите клиент Angular 5, который использует службу RESTful Web API Core 2.
Сторона клиента:
- Угловой 5
- Угловой интерфейс командной строки
- Угловой материал
Сторона сервера:
- .NET С# Веб-API, ядро 2
- Инъекционные зависимости
- JWT-аутентификация
- Сначала код Entity Framework
- SQL-сервер
Примечание
В этом сообщении блога мы предполагаем, что читатель уже имеет базовые знания о TypeScript, модулях Angular, компонентах и импорте/экспорте. Цель этого поста — создать хорошую архитектуру, которая позволит со временем расширять код. |
Что вам нужно?
Начнем с выбора IDE. Конечно, это только мое предпочтение, и вы можете использовать тот, который вам удобнее. В моем случае я буду использовать Visual Studio Code и Visual Studio 2017.
Почему две разные IDE? Поскольку Microsoft создала Visual Studio Code для внешнего интерфейса, я не могу перестать использовать эту IDE. В любом случае, мы также увидим, как интегрировать Angular 5 в проект решения, что поможет вам, если вы относитесь к тому типу разработчиков, которые предпочитают отлаживать как внутреннюю, так и внешнюю часть всего одной клавишей F5.
Что касается серверной части, вы можете установить последнюю версию Visual Studio 2017, которая имеет бесплатную версию для разработчиков, но очень полную: Community.
Итак, вот список вещей, которые нам нужно установить для этого урока:
- Код Visual Studio
- Сообщество Visual Studio 2017 (или любое другое)
- Node.js v8.10.0
- SQL Server 2017
Примечание
Убедитесь, что вы используете как минимум Node 6.9.x и npm 3.xx, запустив node -v и npm -v в окне терминала или консоли. Старые версии выдают ошибки, но новые версии в порядке. |
Передняя часть
Быстрый старт
Да начнется веселье! Первое, что нам нужно сделать, это установить 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
, я рекомендую протестировать сайт с помощью вашего любимого веб-сервиса. Почему? Что ж, некоторые проблемы могут возникнуть только в производственной среде, и создание сайта с помощью ng build
— самый близкий способ приблизиться к этой среде. Затем мы можем открыть папку angular5-app
с кодом Visual Studio и запустить ng build
на терминале bash:
Будет создана новая папка с именем dist
, и мы сможем обслуживать ее с помощью IIS или любого другого веб-сервера, который вы предпочитаете. Затем вы можете ввести URL-адрес в браузере и… готово!
Примечание
Целью этого руководства не является показать, как настроить веб-сервер, поэтому я предполагаю, что у вас уже есть эти знания. |
Папка src
Моя папка src
структурирована следующим образом: внутри папки app
у нас есть components
, где мы создадим для каждого компонента Angular css
, ts
, spec
и html
. Мы также создадим папку config
для сохранения конфигурации сайта, directives
будут содержать все наши пользовательские директивы, helpers
будут содержать общий код, такой как диспетчер аутентификации, layout
будет содержать основные компоненты, такие как тело, заголовок и боковые панели, models
сохранят то, что будет соответствуют моделям представления серверной части, и, наконец, services
будут иметь код для всех обращений к серверной части.
Вне папки app
мы будем хранить папки, созданные по умолчанию, такие как assets
и environments
, а также корневые файлы.
Создание файла конфигурации
Давайте создадим файл config.ts
внутри нашей папки config
и вызовем класс AppConfig
. Здесь мы можем установить все значения, которые мы будем использовать в разных местах нашего кода; например, URL-адрес API. Обратите внимание, что класс реализует свойство 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]; } };
Угловой материал
Прежде чем приступить к макету, давайте настроим каркас компонента пользовательского интерфейса. Конечно, вы можете использовать другие, такие как Bootstrap, но если вам нравится стиль Material, я рекомендую его, потому что он также поддерживается Google.
Чтобы установить его, нам просто нужно запустить следующие три команды, которые мы можем выполнить в терминале Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
Вторая команда связана с тем, что некоторые компоненты Материала зависят от Angular Animations. Я также рекомендую прочитать официальную страницу, чтобы понять, какие браузеры поддерживаются и что такое полифилл.
Третья команда связана с тем, что некоторые компоненты 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";
Теперь импортируйте HammerJS, добавив эту строку в файл main.ts
:
import 'hammerjs';
И, наконец, все, чего нам не хватает, — это добавить значки материалов в index.html
внутри раздела head:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Расположение
В этом примере мы создадим простой макет, подобный этому:
Идея состоит в том, чтобы открыть/скрыть меню, нажав на какую-нибудь кнопку в заголовке. Angular Responsive сделает остальную работу за нас. Для этого мы создадим папку layout
и поместим в нее файлы app.component
, созданные по умолчанию. Но мы также создадим одинаковые файлы для каждого раздела макета, как вы можете видеть на следующем изображении. Затем app.component
будет телом, head.component
— заголовком, а left-panel.component
— меню.
Теперь давайте изменим app.component.html
следующим образом:
<div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>
По сути, у нас будет свойство authentication
в компоненте, которое позволит нам удалить заголовок и меню, если пользователь не вошел в систему, и вместо этого показать простую страницу входа.
head.component.html
выглядит так:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Просто кнопка для выхода пользователя из системы — мы вернемся к этому позже. Что касается left-panel.component.html
, пока просто измените HTML на:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Мы сделали все просто: пока это всего две ссылки для навигации по двум разным страницам. (Мы также вернемся к этому позже.)
Вот как выглядят файлы TypeScript заголовка и левого компонента:
import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }
import { Component } from '@angular/core'; @Component({ selector: 'app-left-panel', templateUrl: './left-panel.component.html', styleUrls: ['./left-panel.component.css'] }) export class LeftPanelComponent { title = 'Angular 5 Seed'; }
Но как насчет кода TypeScript для app.component
? Мы оставим здесь небольшую загадку и приостановим ее на некоторое время, а затем вернемся к ней после реализации аутентификации.
Маршрутизация
Хорошо, теперь у нас есть Angular Material, помогающий нам с пользовательским интерфейсом и простым макетом, чтобы начать создавать наши страницы. Но как мы можем перемещаться между страницами?
Чтобы создать простой пример, давайте создадим две страницы: «Пользователь», где мы можем получить список существующих пользователей в базе данных, и «Панель инструментов», страницу, где мы можем показать некоторую статистику.
Внутри папки 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 {}
Это так просто: просто импортировав RouterModule
и Routes
из @angular/router
, мы можем отобразить пути, которые хотим реализовать. Здесь мы создаем четыре пути:
-
/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
— это глубокая ссылка на страницу сведений о герое, на которой отображается герой с идентификатором: 42.Нет проблем, когда пользователь переходит по этому URL-адресу из запущенного клиента. Маршрутизатор Angular интерпретирует URL-адрес и направляет к этой странице и герою.
Но переход по ссылке в электронном письме, ввод ее в адресную строку браузера или просто обновление браузера на главной странице — все эти действия выполняются самим браузером, а не работающим приложением. Браузер делает прямой запрос на сервер для этого URL-адреса, минуя маршрутизатор.Статический сервер обычно возвращает index.html, когда получает запрос на
http://www.mysite.com/
. Но он отклоняетhttp://www.mysite.com/users/42
и возвращает ошибку 404 — Not Found, если только он не настроен на возврат index.html.
Решить эту проблему очень просто, нам просто нужно создать конфигурацию файла поставщика услуг. Поскольку здесь я работаю с IIS, я покажу вам, как это сделать в этой среде, но концепция аналогична для Apache или любого другого веб-сервера.
Итак, мы создаем файл внутри папки src
с именем web.config
, который выглядит следующим образом:
<?xml version="1.0"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Angular Routes" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> </system.webServer> <system.web> <compilation debug="true"/> </system.web> </configuration>
Затем нам нужно быть уверенным, что этот ассет будет скопирован в развернутую папку. Все, что нам нужно сделать, это изменить наш файл настроек Angular CLI angular-cli.json
:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular5-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico", "web.config" // or whatever equivalent is required by your web server ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
Аутентификация
Вы помните, как мы реализовали класс AuthGuard
для установки конфигурации маршрутизации? Каждый раз, когда мы переходим на другую страницу, мы будем использовать этот класс для проверки подлинности пользователя с помощью токена. Если нет, мы автоматически перенаправим вас на страницу входа. Файл для этого — canActivateAuthGuard.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 доступен только во время сеанса браузера и удаляется при закрытии вкладки или окна; однако он выдерживает перезагрузку страницы. Если данные, которые вы храните, должны быть доступны на постоянной основе, тогда localStorage предпочтительнее, sessionStorage . Файлы 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>
Услуги
На данный момент мы переходим на разные страницы, аутентифицируем нашу клиентскую сторону и отображаем очень простой макет. Но как мы можем получить данные из серверной части? Я настоятельно рекомендую выполнять весь внутренний доступ, в частности, из классов обслуживания . Наша первая служба будет находиться внутри папки services
с именем token.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { AppConfig } from '../config/config'; import { BaseService } from './base.service'; import { Token } from '../models/token'; import { Helpers } from '../helpers/helpers'; @Injectable() export class TokenService extends BaseService { private pathAPI = this.config.setting['PathAPI']; public errorMessage: string; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } auth(data: any): any { let body = JSON.stringify(data); return this.getToken(body); } private getToken (body: any): Observable<any> { return this.http.post<any>(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }
Первый вызов серверной части — это POST-вызов API токена. API токена не нуждается в строке токена в заголовке, но что произойдет, если мы вызовем другую конечную точку? Как вы можете видеть здесь, TokenService
(и классы обслуживания в целом) наследуются от класса BaseService
. Давайте посмотрим на это:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Helpers } from '../helpers/helpers'; @Injectable() export class BaseService { constructor(private helper: Helpers) { } public extractData(res: Response) { let body = res.json(); return body || {}; } public handleError(error: Response | any) { // In a real-world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } public header() { let header = new HttpHeaders({ 'Content-Type': 'application/json' }); if(this.helper.isAuthenticated()) { header = header.append('Authorization', 'Bearer ' + this.helper.getToken()); } return { headers: header }; } public setToken(data:any) { this.helper.setToken(data); } public failToken(error: Response | any) { this.helper.failToken(); return this.handleError(Response); } }
Поэтому каждый раз, когда мы делаем HTTP-вызов, мы реализуем заголовок запроса, просто используя super.header
. Если токен находится в localStorage
, он будет добавлен в заголовок, а если нет, мы просто установим формат JSON. Еще одна вещь, которую мы можем здесь увидеть, это то, что происходит, если аутентификация не удалась.
Компонент входа будет вызывать класс обслуживания, а класс обслуживания будет вызывать серверную часть. Как только у нас будет токен, вспомогательный класс будет управлять токеном, и теперь мы готовы получить список пользователей из нашей базы данных.
Чтобы получить данные из базы данных, сначала убедитесь, что мы сопоставляем классы моделей с моделями внутреннего представления в нашем ответе.
В user.ts
:
export class User { id: number; name: string; }
Теперь мы можем создать файл user.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { BaseService } from './base.service'; import { User } from '../models/user'; import { AppConfig } from '../config/config'; import { Helpers } from '../helpers/helpers'; @Injectable() export class UserService extends BaseService { private pathAPI = this.config.setting['PathAPI']; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } /** GET heroes from the server */ getUsers (): Observable<User[]> { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }
Бэкэнд
Быстрый старт
Добро пожаловать на первый шаг нашего приложения Web API Core 2. Первое, что нам нужно, это создать основное веб-приложение ASP.Net, которое мы назовем SeedAPI.Web.API
.
Обязательно выберите пустой шаблон для чистого старта, как показано ниже:
Вот и все, мы создаем решение, начиная с пустого веб-приложения. Теперь наша архитектура будет такой, как мы перечисляем ниже, поэтому нам нужно будет создать разные проекты:
Для этого для каждого просто щелкните правой кнопкой мыши решение и добавьте проект «Библиотека классов (.NET Core)».
Архитектура
В предыдущем разделе мы создали восемь проектов, но для чего они? Вот простое описание каждого из них:
-
Web.API
: это наш стартовый проект, в котором создаются конечные точки. Здесь мы настроим JWT, зависимости внедрения и контроллеры. -
ViewModels
: здесь мы выполняем преобразования из типа данных, которые контроллеры будут возвращать в ответах на внешний интерфейс. Рекомендуется сопоставлять эти классы с внешними моделями. -
Interfaces
: это будет полезно при реализации зависимостей инъекций. Неоспоримым преимуществом статически типизированного языка является то, что компилятор может помочь проверить, действительно ли выполняется контракт, на который опирается ваш код. -
Commons
: здесь будут все общие поведения и служебный код. -
Models
: рекомендуется не сопоставлять базу данных напрямую сViewModels
, поэтому цельюModels
является создание классов базы данных сущностей, независимых от внешнего интерфейса. Это позволит нам в будущем изменять нашу базу данных без обязательного влияния на наш внешний интерфейс. Это также помогает, когда мы просто хотим провести рефакторинг. -
Maps
: здесь мы сопоставляемViewModels
сModels
и наоборот. Этот шаг вызывается между контроллерами и службами. -
Services
: библиотека для хранения всей бизнес-логики. -
Repositories
: это единственное место, где мы вызываем базу данных.
Ссылки будут выглядеть так:
Аутентификация на основе JWT
В этом разделе мы увидим базовую настройку аутентификации с помощью токена и немного углубимся в тему безопасности.
Чтобы начать настройку веб-токена JSON (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/" }
Затем нам нужно только вызвать его из метода ConfigureServices
в 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(); }
Теперь мы готовы создать наш первый контроллер с именем TokenController.cs
. Значение, которое мы установили в appsettings.json
в "veryVerySecretKey"
, должно совпадать с тем, которое мы используем для создания токена, но сначала давайте создадим LoginViewModel
внутри нашего проекта ViewModels
:
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
просто жестко закодирована проверка пользователя на данный момент, но нам нужно будет вызвать базу данных, чтобы проверить ее в конце.
Контекст приложения
Настроить Entity Framework очень просто, так как Microsoft выпустила версию Core 2.0 — для краткости EF Core 2 . Мы собираемся углубиться в модель code-first с использованием identityDbContext
, поэтому сначала убедитесь, что вы установили все зависимости. Вы можете использовать NuGet для управления им:
Используя проект Models
, мы можем создать здесь внутри папки Context
два файла: ApplicationContext.cs
и IApplicationContext.cs
. Также нам понадобится класс EntityBase
.
Файлы EntityBase
будут наследоваться каждой моделью сущности, но User.cs
— это класс удостоверений и единственная сущность, которая будет наследоваться от IdentityUser
. Ниже представлены оба класса:
namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } }
namespace SeedAPI.Models.EntityBase { public class EntityBase { public DateTime? Created { get; set; } public DateTime? Updated { get; set; } public bool Deleted { get; set; } public EntityBase() { Deleted = false; } public virtual int IdentityID() { return 0; } public virtual object[] IdentityID(bool dummy = true) { return new List<object>().ToArray(); } } }
Теперь мы готовы создать ApplicationContext.cs
, который будет выглядеть так:
namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext<User>, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet<User> UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet<T> Set<T>() where T : class { return base.Set<T>(); } public void BeginTransaction() { dbContextTransaction = Database.BeginTransaction(); } public void CommitTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Commit(); } } public void RollbackTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Rollback(); } } public void DisposeTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Dispose(); } } } }
Мы действительно близки, но сначала нам нужно будет создать больше классов, на этот раз в папке App_Start
расположенной в проекте Web.API
. Первый класс предназначен для инициализации контекста приложения, а второй — для создания образцов данных только для целей тестирования во время разработки.
namespace SeedAPI.Web.API.App_Start { public class DBContextConfig { public static void Initialize(IConfiguration configuration, IHostingEnvironment env, IServiceProvider svp) { var optionsBuilder = new DbContextOptionsBuilder(); if (env.IsDevelopment()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsStaging()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsProduction()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); var context = new ApplicationContext(optionsBuilder.Options); if(context.Database.EnsureCreated()) { IUserMap service = svp.GetService(typeof(IUserMap)) as IUserMap; new DBInitializeConfig(service).DataTest(); } } public static void Initialize(IServiceCollection services, IConfiguration configuration) { services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); } } }
namespace SeedAPI.Web.API.App_Start { public class DBInitializeConfig { private IUserMap userMap; public DBInitializeConfig (IUserMap _userMap) { userMap = _userMap; } public void DataTest() { Users(); } private void Users() { userMap.Create(new UserViewModel() { id = 1, name = "Pablo" }); userMap.Create(new UserViewModel() { id = 2, name = "Diego" }); } } }
And we call them from our startup file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); } // ... // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider svp) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } DBContextConfig.Initialize(Configuration, env, svp); app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); app.UseAuthentication(); app.UseMvc(); }
Внедрение зависимости
It is a good practice to use dependency injection to move among different projects. This will help us to communicate between controllers and mappers, mappers and services, and services and repositories.
Inside the folder App_Start
we will create the file DependencyInjectionConfig.cs
and it will look like this:
namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped<IApplicationContext, ApplicationContext>(); services.AddScoped<IUserMap, UserMap>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserRepository, UserRepository>(); } } }
We will need to create for each new entity a new Map
, Service
, and Repository
, and match them to this file. Then we just need to call it from the startup.cs
file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }
Finally, when we need to get the users list from the database, we can create a controller using this dependency injection:
namespace SeedAPI.Web.API.Controllers { [Route("api/[controller]")] [Authorize] public class UserController : Controller { IUserMap userMap; public UserController(IUserMap map) { userMap = map; } // GET api/user [HttpGet] public IEnumerable<UserViewModel> Get() { return userMap.GetAll(); ; } // GET api/user/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/user [HttpPost] public void Post([FromBody]string user) { } // PUT api/user/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string user) { } // DELETE api/user/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
Look how the Authorize
attribute is present here to be sure that the front end has logged in and how dependency injection works in the constructor of the class.
We finally have a call to the database but first, we need to understand the Map
project.
Проект « Maps
»
Этот шаг предназначен только для сопоставления ViewModels
с моделями баз данных и обратно. Мы должны создать по одному для каждой сущности, и, следуя нашему предыдущему примеру, файл UserMap.cs
будет выглядеть так:
namespace SeedAPI.Maps { public class UserMap : IUserMap { IUserService userService; public UserMap(IUserService service) { userService = service; } public UserViewModel Create(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return DomainToViewModel(userService.Create(user)); } public bool Update(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return userService.Update(user); } public bool Delete(int id) { return userService.Delete(id); } public List<UserViewModel> GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List<UserViewModel> DomainToViewModel(List<User> domain) { List<UserViewModel> model = new List<UserViewModel>(); foreach (User of in domain) { model.Add(DomainToViewModel(of)); } return model; } public User ViewModelToDomain(UserViewModel officeViewModel) { User domain = new User(); domain.Name = officeViewModel.name; return domain; } } }
Похоже, в конструкторе класса снова работает внедрение зависимостей, связывающее Maps с проектом Services.
Проект Services
Здесь нечего сказать: наш пример очень прост, и у нас нет бизнес-логики или кода для написания здесь. Этот проект окажется полезным в будущих расширенных требованиях, когда нам нужно будет рассчитать или выполнить некоторую логику до или после шагов базы данных или контроллера. Следуя примеру, класс будет выглядеть довольно голым:
namespace SeedAPI.Services { public class UserService : IUserService { private IUserRepository repository; public UserService(IUserRepository userRepository) { repository = userRepository; } public User Create(User domain) { return repository.Save(domain); } public bool Update(User domain) { return repository.Update(domain); } public bool Delete(int id) { return repository.Delete(id); } public List<User> GetAll() { return repository.GetAll(); } } }
Проект Repositories
Мы подошли к последнему разделу этого руководства: нам просто нужно сделать вызовы к базе данных, поэтому мы создаем файл UserRepository.cs
, где мы можем читать, вставлять или обновлять пользователей в базе данных.
namespace SeedAPI.Repositories { public class UserRepository : BaseRepository, IUserRepository { public UserRepository(IApplicationContext context) : base(context) { } public User Save(User domain) { try { var us = InsertUser<User>(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser<User>(domain); return true; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Delete(int id) { try { User user = Context.UsersDB.Where(x => x.Id.Equals(id)).FirstOrDefault(); if (user != null) { //Delete<User>(user); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List<User> GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } }
Резюме
В этой статье я объяснил, как создать хорошую архитектуру с использованием Angular 5 и Web API Core 2. На этом этапе вы создали основу для большого проекта с кодом, который поддерживает значительный рост требований.
Правда в том, что ничто не может конкурировать с JavaScript на фронтенде, а что может конкурировать с C#, если вам нужна поддержка SQL Server и Entity Framework на бэкенде? Итак, идея этой статьи заключалась в том, чтобы объединить лучшее из двух миров, и я надеюсь, вам понравилось.
Что дальше?
Если вы работаете в команде разработчиков Angular, возможно, разные разработчики работают над интерфейсом и сервером, поэтому хорошей идеей для синхронизации усилий обеих команд может быть интеграция Swagger с Web API 2. Swagger — отличный инструмент для документирования и тестирования ваших RESTFul API. Прочтите руководство Microsoft: Начало работы с Swashbuckle и ASP.NET Core.
Если вы все еще новичок в Angular 5 и у вас возникли проблемы со следованием, прочитайте Учебное пособие по Angular 5: пошаговое руководство по вашему первому приложению Angular 5, написанное товарищем Toptaler Сергеем Моисеевым.