Angular 5 e ASP.NET Core

Publicados: 2022-03-11

Estou pensando em escrever um post no blog desde que a primeira versão do Angular praticamente matou a Microsoft no lado do cliente. Tecnologias como ASP.Net, Web Forms e MVC Razor tornaram-se obsoletas, substituídas por uma estrutura JavaScript que não é exatamente Microsoft. No entanto, desde a segunda versão do Angular, a Microsoft e o Google estão trabalhando juntos para criar o Angular 2, e foi aí que minhas duas tecnologias favoritas começaram a trabalhar juntas.

Neste blog, quero ajudar as pessoas a criar a melhor arquitetura combinando esses dois mundos. Você está pronto? Aqui vamos nós!

Sobre a arquitetura

Você construirá um cliente Angular 5 que consome um serviço RESTful Web API Core 2.

O lado do cliente:

  • Angular 5
  • CLI angular
  • Material Angular

O lado do servidor:

  • .NET C# Web API Core 2
  • Dependências de injeção
  • Autenticação JWT
  • Código da estrutura da entidade primeiro
  • servidor SQL

Observação

Nesta postagem do blog, estamos assumindo que o leitor já possui conhecimento básico de TypeScript, módulos Angular, componentes e importação/exportação. O objetivo deste post é criar uma boa arquitetura que permita que o código cresça ao longo do tempo.

O que você precisa?

Vamos começar escolhendo o IDE. Claro, esta é apenas a minha preferência, e você pode usar o que você se sentir mais confortável. No meu caso, usarei o Visual Studio Code e o Visual Studio 2017.

Por que dois IDEs diferentes? Como a Microsoft criou o Visual Studio Code para o front-end, não consigo parar de usar este IDE. De qualquer forma, veremos também como integrar o Angular 5 dentro do projeto da solução, que irá ajudá-lo se você é o tipo de desenvolvedor que prefere depurar tanto o back-end quanto o front com apenas um F5.

Sobre o back-end, você pode instalar a versão mais recente do Visual Studio 2017 que possui uma edição gratuita para desenvolvedores, mas é bem completa: Community.

Então, aqui a lista de coisas que precisamos instalar para este tutorial:

  • Código do Visual Studio
  • Comunidade do Visual Studio 2017 (ou qualquer)
  • Node.js v8.10.0
  • SQL Server 2017

Observação

Verifique se você está executando pelo menos Node 6.9.xe npm 3.xx executando node -v e npm -v em um terminal ou janela de console. As versões mais antigas produzem erros, mas as versões mais recentes funcionam bem.

O Front-End

Começo rápido

E que comece a diversão! A primeira coisa que precisamos fazer é instalar o Angular CLI globalmente, então abra o prompt de comando node.js e execute este comando:

 npm install -g @angular/cli

Ok, agora temos nosso empacotador de módulos. Isso geralmente instala o módulo em sua pasta de usuário. Um alias não deve ser necessário por padrão, mas se precisar, você pode executar a próxima linha:

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

O próximo passo é criar o novo projeto. Vou chamá-lo angular5-app . Primeiro, navegamos até a pasta na qual queremos criar o site e, em seguida:

 ng new angular5-app

Primeira construção

Embora você possa testar seu novo site apenas executando ng serve --open , eu recomendo testar o site a partir do seu serviço web favorito. Por quê? Bem, alguns problemas podem acontecer apenas em produção, e construir o site com ng build é a maneira mais próxima de abordar esse ambiente. Em seguida, podemos abrir a pasta angular5-app com o Visual Studio Code e executar ng build no terminal bash:

construindo o aplicativo angular pela primeira vez

Uma nova pasta chamada dist será criada e podemos servi-la usando o IIS ou qualquer servidor web que você preferir. Então você pode digitar a URL no navegador e… pronto!

a nova estrutura de diretórios

Observação

Não é o propósito deste tutorial mostrar como configurar um servidor web, então suponho que você já tenha esse conhecimento.

Tela de boas-vindas do Angular 5

A pasta src

A estrutura de pastas src

Minha pasta src está estruturada da seguinte forma: Dentro da pasta app temos components onde criaremos para cada componente Angular os arquivos css , ts , spec e html . Também criaremos uma pasta de config para manter a configuração do site, as directives terão todas as nossas diretivas personalizadas, os helpers o código comum como o gerenciador de autenticação, o layout conterá os componentes principais como o corpo, a cabeça e os painéis laterais, os models mantêm o que será combinar com os modelos de exibição de back-end e, finalmente, os services terão o código para todas as chamadas para o back-end.

Fora da pasta do app vamos manter as pastas criadas por padrão, como assets e environments , e também os arquivos root.

Criando o arquivo de configuração

Vamos criar um arquivo config.ts dentro da nossa pasta config e chamar a classe AppConfig . É aqui que podemos definir todos os valores que usaremos em diferentes locais do nosso código; por exemplo, o URL da API. Observe que a classe implementa uma propriedade get que recebe, como parâmetro, uma estrutura chave/valor e um método simples para acessar o mesmo valor. Dessa forma, será fácil obter os valores apenas chamando this.config.setting['PathAPI'] das classes que herdam dele.

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

Material Angular

Antes de iniciar o layout, vamos configurar a estrutura do componente de interface do usuário. Claro, você pode usar outros como o Bootstrap, mas se você gosta do estilo do Material, eu o recomendo porque também é suportado pelo Google.

Para instalá-lo, basta executar os próximos três comandos, que podemos executar no terminal do Visual Studio Code:

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

O segundo comando é porque alguns componentes de material dependem de animações angulares. Também recomendo a leitura da página oficial para entender quais navegadores são suportados e o que é um polyfill.

O terceiro comando é porque alguns componentes do Material dependem do HammerJS para gestos.

Agora podemos proceder para importar os módulos do componente que queremos usar em nosso arquivo 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 ],

O próximo passo é alterar o arquivo style.css , adicionando o tipo de tema que você deseja usar:

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

Agora importe o HammerJS adicionando esta linha no arquivo main.ts :

 import 'hammerjs';

E, finalmente, tudo o que falta é adicionar os ícones do Material ao index.html , dentro da seção head:

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

O layout

Neste exemplo, vamos criar um layout simples como este:

Exemplo de layout

A ideia é abrir/ocultar o menu clicando em algum botão no cabeçalho. A Angular Responsive fará o resto do trabalho para nós. Para isso vamos criar uma pasta layout e colocar dentro dela os arquivos app.component criados por padrão. Mas também criaremos os mesmos arquivos para cada seção do layout, como você pode ver na próxima imagem. Então, app.component será o corpo, head.component o cabeçalho e left-panel.component o menu.

Pasta de configuração destacada

Agora vamos alterar app.component.html da seguinte forma:

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

Basicamente teremos uma propriedade de authentication no componente que nos permitirá remover o cabeçalho e o menu se o usuário não estiver logado e, em vez disso, mostrar uma página de login simples.

O head.component.html se parece com isso:

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

Apenas um botão para desconectar o usuário - voltaremos a isso mais tarde. Quanto a left-panel.component.html , por enquanto apenas altere o HTML para:

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

Mantivemos simples: até agora são apenas dois links para navegar por duas páginas diferentes. (Também voltaremos a isso mais tarde.)

Agora, é assim que os arquivos TypeScript do componente principal e do lado esquerdo se parecem:

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

Mas e o código TypeScript para app.component ? Deixaremos um pequeno mistério aqui e pausaremos por um tempo, e voltaremos a isso depois de implementar a autenticação.

Roteamento

Ok, agora temos o Angular Material nos ajudando com a interface do usuário e um layout simples para começar a construir nossas páginas. Mas como podemos navegar entre as páginas?

Para criar um exemplo simples, vamos criar duas páginas: “User”, onde podemos obter uma lista dos usuários existentes no banco de dados, e “Dashboard”, uma página onde podemos mostrar algumas estatísticas.

Dentro da pasta app vamos criar um arquivo chamado app-routing.modules.ts assim:

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

É simples assim: apenas importando RouterModule e Routes de @angular/router , podemos mapear os caminhos que queremos implementar. Aqui estamos criando quatro caminhos:

  • /dashboard : Nossa página inicial
  • /login : A página onde o usuário pode autenticar
  • /logout : Um caminho simples para desconectar o usuário
  • /users : Nossa primeira página onde queremos listar os usuários do back-end

Observe que o dashboard é nossa página por padrão, portanto, se o usuário digitar a URL / , a página será redirecionada automaticamente para esta página. Além disso, dê uma olhada no parâmetro canActivate : Aqui estamos criando uma referência para a classe AuthGuard , que nos permitirá verificar se o usuário está logado. Caso contrário, ele redireciona para a página de login. Na próxima seção, mostrarei como criar essa classe.

Agora, tudo o que precisamos fazer é criar o menu. Lembra na seção de layout quando criamos o arquivo left-panel.component.html para ficar assim?

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

Aqui é onde nosso código encontra a realidade. Agora podemos construir o código e testá-lo na URL: Você deve ser capaz de navegar da página Dashboard para Users, mas o que acontece se você digitar a URL our.site.url/users diretamente no navegador?

texto alternativo da imagem

Observe que esse erro também aparece se você atualizar o navegador depois de navegar com sucesso para esse URL por meio do painel lateral do aplicativo. Para entender esse erro, permita-me consultar os documentos oficiais onde é realmente claro:

Um aplicativo roteado deve oferecer suporte a links diretos. Um link direto é uma URL que especifica um caminho para um componente dentro do aplicativo. Por exemplo, http://www.mysite.com/users/42 é um link direto para a página de detalhes do herói que exibe o herói com id: 42.

Não há problema quando o usuário navega para essa URL de dentro de um cliente em execução. O roteador Angular interpreta a URL e roteia para essa página e herói.

Mas clicar em um link em um e-mail, inseri-lo na barra de endereços do navegador ou simplesmente atualizar o navegador enquanto estiver na página de detalhes do herói – todas essas ações são tratadas pelo próprio navegador, fora do aplicativo em execução. O navegador faz uma solicitação direta ao servidor para essa URL, ignorando o roteador.

Um servidor estático rotineiramente retorna index.html quando recebe uma solicitação para http://www.mysite.com/ . Mas ele rejeita http://www.mysite.com/users/42 e retorna um erro 404 - Not Found, a menos que esteja configurado para retornar index.html.

Para corrigir esse problema é muito simples, basta criar o arquivo de configuração do provedor de serviços. Já que estou trabalhando com IIS aqui, vou mostrar como fazer neste ambiente, mas o conceito é semelhante para Apache ou qualquer outro servidor web.

Então criamos um arquivo dentro da pasta src chamado web.config que se parece com isso:

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

Em seguida, precisamos ter certeza de que esse ativo será copiado para a pasta implantada. Tudo o que precisamos fazer é alterar nosso arquivo de configurações 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": {} } }

Autenticação

Você se lembra de como implementamos a classe AuthGuard para definir a configuração de roteamento? Toda vez que navegarmos para uma página diferente, usaremos essa classe para verificar se o usuário está autenticado com um token. Caso contrário, redirecionaremos automaticamente para a página de login. O arquivo para isso é canActivateAuthGuard.ts — crie-o dentro da pasta helpers e deixe-o assim:

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

Assim, toda vez que alterarmos a página será chamado o método canActivate , que verificará se o usuário está autenticado, caso contrário, utilizamos nossa instância do Router para redirecionar para a página de login. Mas o que é esse novo método na classe Helper ? Sob a pasta helpers vamos criar um arquivo helpers.ts . Aqui precisamos gerenciar localStorage , onde armazenaremos o token que recebemos do back-end.

Observação

Em relação ao localStorage , você também pode usar cookies ou sessionStorage , e a decisão dependerá do comportamento que queremos implementar. Como o nome sugere, sessionStorage está disponível apenas durante a sessão do navegador e é excluído quando a guia ou janela é fechada; ele, no entanto, sobrevive a recargas de página. Se os dados que você está armazenando precisam estar disponíveis continuamente, localStorage é preferível a sessionStorage . Os cookies são principalmente para leitura do lado do servidor, enquanto localStorage só pode ser lido do lado do cliente. Portanto, a questão é, em seu aplicativo, quem precisa desses dados --- o cliente ou o servidor?


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

Nosso código de autenticação está fazendo sentido agora? Voltaremos à classe Subject mais tarde, mas agora vamos voltar por um minuto para a configuração de roteamento. Dê uma olhada nesta linha:

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

Este é o nosso componente para sair do site, e é apenas uma classe simples para limpar o localStorage . Vamos criá-lo na pasta components/login com o nome de 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']); } }

Assim, toda vez que formos para a URL /logout , o localStorage será removido e o site redirecionará para a página de login. Finalmente, vamos criar login.component.ts assim:

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

Como você pode ver, no momento nós codificamos nossas credenciais aqui. Observe que aqui estamos chamando uma classe de serviço; vamos criar essas classes de serviços para ter acesso ao nosso back-end na próxima seção.

Por fim, precisamos voltar ao arquivo app.component.ts , o layout do site. Aqui, se o usuário estiver autenticado, ele mostrará as seções de menu e cabeçalho, mas se não, o layout mudará para mostrar apenas nossa página de login.

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

Lembre-se da classe Subject em nossa classe auxiliar? Este é um Observable . Observable fornecem suporte para a transmissão de mensagens entre editores e assinantes em seu aplicativo. Sempre que o token de autenticação for alterado, a propriedade de authentication será atualizada. Revendo o arquivo app.component.html , provavelmente fará mais sentido agora:

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

Serviços

Neste ponto, estamos navegando para diferentes páginas, autenticando nosso lado do cliente e renderizando um layout muito simples. Mas como podemos obter dados do back-end? Eu recomendo fortemente fazer todo o acesso de back-end das classes de serviço em particular. Nosso primeiro serviço estará dentro da pasta services , chamada 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) ); } }

A primeira chamada para o back-end é uma chamada POST para a API de token. A API de token não precisa da string de token no cabeçalho, mas o que acontece se chamarmos outro endpoint? Como você pode ver aqui, TokenService (e classes de serviço em geral) herdam da classe BaseService . Vamos dar uma olhada nisso:

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

Assim, toda vez que fazemos uma chamada HTTP, implementamos o cabeçalho da solicitação usando apenas super.header . Se o token estiver em localStorage , ele será anexado dentro do cabeçalho, mas se não estiver, apenas definiremos o formato JSON. Outra coisa que podemos ver aqui é o que acontece se a autenticação falhar.

O componente de login chamará a classe de serviço e a classe de serviço chamará o back-end. Assim que tivermos o token, a classe auxiliar gerenciará o token e agora estamos prontos para obter a lista de usuários do nosso banco de dados.

Para obter dados do banco de dados, primeiro certifique-se de combinar as classes de modelo com os modelos de exibição de back-end em nossa resposta.

Em user.ts :

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

E podemos criar agora o arquivo 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)); }

A extremidade traseira

Começo rápido

Bem-vindo à primeira etapa do nosso aplicativo Web API Core 2. A primeira coisa que precisamos é criar um ASP.Net Core Web Application, que chamaremos de SeedAPI.Web.API .

Criando um novo arquivo

Certifique-se de escolher o modelo Vazio para um começo limpo, como você pode ver abaixo:

escolha o modelo Vazio

Isso é tudo, criamos a solução começando com uma aplicação web vazia. Agora nossa arquitetura ficará como listamos abaixo então teremos que criar os diferentes projetos:

nossa arquitetura atual

Para fazer isso, para cada um basta clicar com o botão direito do mouse na Solução e adicionar um projeto “Biblioteca de Classes (.NET Core)”.

adicione uma "Biblioteca de Classes (.NET Core)"

A arquitetura

Na seção anterior criamos oito projetos, mas para que servem? Aqui está uma descrição simples de cada um:

  • Web.API : Este é o nosso projeto de inicialização e onde os endpoints são criados. Aqui vamos configurar JWT, dependências de injeção e controladores.
  • ViewModels : Aqui realizamos conversões a partir do tipo de dados que os controladores retornarão nas respostas ao front end. É uma boa prática combinar essas classes com os modelos front-end.
  • Interfaces : Isso será útil na implementação de dependências de injeção. O benefício atraente de uma linguagem de tipagem estática é que o compilador pode ajudar a verificar se um contrato do qual seu código depende é realmente atendido.
  • Commons : Todos os comportamentos compartilhados e códigos utilitários estarão aqui.
  • Models : é uma boa prática não corresponder o banco de dados diretamente com os ViewModels voltados para o front-end, portanto, o objetivo de Models é criar classes de banco de dados de entidade independentes do front-end. Isso nos permitirá, no futuro, alterar nosso banco de dados sem necessariamente afetar nosso front-end. Também ajuda quando simplesmente queremos fazer alguma refatoração.
  • Maps : Aqui é onde ViewModels para Models e vice-versa. Esta etapa é chamada entre controladores e Serviços.
  • Services : Uma biblioteca para armazenar toda a lógica de negócios.
  • Repositories : Este é o único lugar onde chamamos o banco de dados.

As referências ficarão assim:

Diagrama de referências

Autenticação baseada em JWT

Nesta seção, veremos a configuração básica de autenticação de token e aprofundaremos um pouco mais o assunto de segurança.

Para começar a configurar o JSON web token (JWT) vamos criar a próxima classe dentro da pasta App_Start chamada JwtTokenConfig.cs . O código interno ficará assim:

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

Os valores dos parâmetros de validação dependerão da exigência de cada projeto. O usuário e público válidos que podemos definir lendo o arquivo de configuração appsettings.json :

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

Então, precisamos apenas chamá-lo do método ConfigureServices em 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(); }

Agora estamos prontos para criar nosso primeiro controlador chamado TokenController.cs . O valor que definimos em appsettings.json como "veryVerySecretKey" deve corresponder ao que usamos para criar o token, mas primeiro vamos criar o LoginViewModel dentro do nosso projeto ViewModels :

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

E finalmente o controlador:

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

O método BuildToken criará o token com o código de segurança fornecido. O método Authenticate tem apenas a validação do usuário codificada no momento, mas precisaremos chamar o banco de dados para validá-lo no final.

O contexto do aplicativo

Configurar o Entity Framework é muito fácil desde que a Microsoft lançou a versão Core 2.0— EF Core 2 para abreviar. Vamos nos aprofundar em um modelo code-first usando identityDbContext , portanto, primeiro certifique-se de ter instalado todas as dependências. Você pode usar o NuGet para gerenciá-lo:

Obtendo dependências

Usando o projeto Models podemos criar aqui dentro da pasta Context dois arquivos, ApplicationContext.cs e IApplicationContext.cs . Além disso, precisaremos de uma classe EntityBase .

Aulas

Os arquivos EntityBase serão herdados por cada modelo de entidade, mas User.cs é uma classe de identidade e a única entidade que herdará de IdentityUser . Abaixo estão as duas classes:

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

Agora estamos prontos para criar ApplicationContext.cs , que ficará assim:

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

Estamos muito próximos, mas primeiro precisaremos criar mais classes, desta vez na pasta App_Start localizada no projeto Web.API . A primeira classe é inicializar o contexto do aplicativo e a segunda é criar dados de amostra apenas para fins de teste durante o desenvolvimento.

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

Injeção de dependência

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.

O Projeto Maps

Esta etapa é apenas para mapear ViewModels de e para modelos de banco de dados. Devemos criar um para cada entidade e, seguindo nosso exemplo anterior, o arquivo UserMap.cs ficará assim:

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

Parece que mais uma vez, a injeção de dependência está funcionando no construtor da classe, vinculando o Maps ao projeto Services.

O Projeto Services

Não há muito a dizer aqui: Nosso exemplo é muito simples e não temos lógica de negócios ou código para escrever aqui. Este projeto seria útil em futuros requisitos avançados quando precisarmos calcular ou fazer alguma lógica antes ou depois das etapas do banco de dados ou do controlador. Seguindo o exemplo, a classe ficará bem vazia:

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

O Projeto Repositories

Estamos chegando à última seção deste tutorial: só precisamos fazer chamadas para o banco de dados, então criamos um arquivo UserRepository.cs onde podemos ler, inserir ou atualizar usuários no banco de dados.

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

Resumo

Neste artigo, expliquei como criar uma boa arquitetura usando Angular 5 e Web API Core 2. Neste ponto, você criou a base para um grande projeto com código que suporta um grande crescimento de requisitos.

A verdade é que nada compete com o JavaScript no front-end e o que pode competir com o C# se você precisar do suporte do SQL Server e do Entity Framework no back-end? Então a ideia deste artigo foi combinar o melhor de dois mundos e espero que tenham gostado.

Qual é o próximo?

Se você está trabalhando em uma equipe de desenvolvedores Angular provavelmente pode haver diferentes desenvolvedores trabalhando no front-end e no back-end, então uma boa ideia para sincronizar os esforços de ambas as equipes pode ser integrar o Swagger com a API Web 2. Swagger é um ótimo ferramenta para documentar e testar suas APIs RESTFul. Leia o guia da Microsoft: Introdução ao Swashbuckle e ASP.NET Core.

Se você ainda é muito novo no Angular 5 e está tendo problemas para acompanhar, leia An Angular 5 Tutorial: Step by Step Guide to Your First Angular 5 App pelo colega Toptaler Sergey Moiseev.