Tenho pensado em escrever uma postagem 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 se tornaram obsoletas, substituídas por uma estrutura JavaScript que não é exatamente a Microsoft. No entanto, desde a segunda versão do Angular, a Microsoft e o Google têm trabalhado juntos para criar o Angular 2, e foi quando minhas duas tecnologias favoritas começaram a funcionar juntas.
Neste blog, quero ajudar as pessoas a criar a melhor arquitetura combinando esses dois mundos. Você está pronto? Aqui vamos nós!
Você construirá um cliente Angular 5 que consome um serviço RESTful Web API Core 2.
O lado do cliente:
O lado do servidor:
Nota
Nesta postagem do blog, presumimos que o leitor já tenha conhecimento básico de TypeScript, módulos Angular, componentes e importação / exportação. O objetivo desta postagem é criar uma boa arquitetura que permitirá que o código cresça com o tempo. |
Vamos começar escolhendo o IDE. Claro, essa é apenas minha preferência, e você pode usar aquele com o qual 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 posso parar de usar esse IDE. De qualquer forma, veremos também como integrar o Angular 5 dentro do projeto da solução, o que o ajudará se você é o tipo de desenvolvedor que prefere depurar back end e front com apenas um F5.
Sobre o back-end, você pode instalar a versão mais recente do Visual Studio 2017, que tem uma edição gratuita para desenvolvedores, mas é muito completa: Comunidade.
Então, aqui está a lista de coisas que precisamos instalar para este tutorial:
Nota
Verifique se você está executando pelo menos o Nó 6.9.xe npm 3.x.x executando node -v e npm -v em um terminal ou janela de console. Versões mais antigas produzem erros, mas versões mais recentes funcionam. |
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ódulo. 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='/.npm/lib/node_modules/angular-cli/bin/ng'
A próxima etapa é criar o novo projeto. Vou chamá-lo de angular5-app
. Primeiro, navegamos até a pasta sob a qual queremos criar o site e, em seguida:
ng new angular5-app
Embora você possa testar seu novo site apenas executando ng serve --open
, eu recomendo testar o site de seu serviço da Web favorito. Por quê? Bem, alguns problemas podem acontecer apenas na produção e na construção do site com ng build
é a forma mais próxima de abordar esse ambiente. Em seguida, podemos abrir a pasta angular5-app
com o Visual Studio Code e execute ng build
no terminal bash:
Uma nova pasta chamada dist
será criado e podemos atendê-lo usando IIS ou qualquer servidor web de sua preferência. Depois, você pode digitar a URL no navegador e ... pronto!
pequenos dados vs big data
Nota
Não é o objetivo deste tutorial mostrar como configurar um servidor web, então presumo que você já tenha esse conhecimento. |
src
Pasta
Meu src
A pasta é estruturada da seguinte maneira: Dentro de app
pasta que temos components
onde criaremos para cada componente angular css
, ts
, spec
e html
arquivos. Também criaremos um config
pasta para manter a configuração do site, directives
terá todas as nossas diretivas personalizadas, helpers
hospedará código comum como o gerenciador de autenticação, layout
conterá os principais componentes, como corpo, cabeça e painéis laterais, models
mantém o que vai corresponder aos modelos de visualização de back-end e, finalmente, services
terá o código para todas as chamadas para o back end.
Fora do app
manteremos as pastas criadas por padrão, como assets
e environments
, e também os arquivos raiz.
Vamos criar um config.ts
arquivo dentro de nosso config
pasta e chame a classe AppConfig
. É aqui que podemos definir todos os valores que usaremos em diferentes lugares em nosso código; por exemplo, o URL da API. Observe que a classe implementa um get
propriedade que recebe, como parâmetro, uma estrutura chave / valor e um método simples para obter acesso ao 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]; } };
Antes de iniciar o layout, vamos configurar a estrutura do componente de IU. Claro, você pode usar outros como Bootstrap, mas se você gosta do estilo do Material, eu o recomendo porque também é compatível com o Google.
Para instalá-lo, precisamos apenas executar os próximos três comandos, que podemos executar no terminal de código do Visual Studio:
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. Eu também recomendo a leitura a página oficial para entender quais navegadores são compatíveis e o que é um polyfill.
O terceiro comando é porque alguns componentes de Material contam com HammerJS para gestos.
Agora podemos prosseguir com a importação dos módulos de componentes que desejamos usar em nosso app.module.ts
Arquivo:
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 ],
A próxima etapa é alterar o style.css
arquivo, adicionando o tipo de tema que deseja usar:
@import ' [email protected] /material/prebuilt-themes/deeppurple-amber.css';
Agora importe o HammerJS adicionando esta linha no main.ts
Arquivo:
import 'hammerjs';
E, finalmente, tudo o que falta é adicionar os ícones de material a index.html
, dentro da seção head:
layout
Neste exemplo, criaremos um layout simples como este:
A ideia é abrir / ocultar o menu clicando em algum botão do cabeçalho. O Angular Responsive fará o resto do trabalho por nós. Para fazer isso, criaremos um app.component
e coloque dentro dela o app.component
arquivos 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, head.component
será o corpo, left-panel.component
o cabeçalho e app.component.html
o cardápio.
Agora vamos mudar Menu
do seguinte modo:
authentication
Basicamente, teremos um head.component.html
propriedade 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 Logout!
se parece com isso:
left-panel.component.html
Apenas um botão para desconectar o usuário - vamos voltar a isso mais tarde. Quanto a Dashboard Users
, por enquanto apenas altere o HTML para:
import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }
Mantivemos as coisas simples: até agora, são apenas dois links para navegar por duas páginas diferentes. (Também voltaremos a isso mais tarde.)
Agora, esta é a aparência dos arquivos TypeScript do componente principal e do lado esquerdo:
import { Component } from '@angular/core'; @Component({ selector: 'app-left-panel', templateUrl: './left-panel.component.html', styleUrls: ['./left-panel.component.css'] }) export class LeftPanelComponent { title = 'Angular 5 Seed'; }
app.component
Mas e quanto ao código TypeScript para app
? Vamos deixar um pequeno mistério aqui e pausar por um tempo, e voltar a isso após implementar a autenticação.
Ok, agora temos Angular Material nos ajudando com a IU 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: “Usuário”, onde podemos obter uma lista dos usuários existentes no banco de dados, e “Painel”, uma página onde podemos mostrar algumas estatísticas.
Dentro do app-routing.modules.ts
pasta vamos criar um arquivo chamado 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 {}
parecido com isto:
RouterModule
É simples assim: basta importar Routes
e @angular/router
de /dashboard
, podemos mapear os caminhos que queremos implementar. Aqui estamos criando quatro caminhos:
/login
: Nossa página inicial/logout
: A página onde o usuário pode autenticar/users
: Um caminho simples para desconectar o usuáriodashboard
: Nossa primeira página, onde queremos listar os usuários do back endObserve que /
é nossa página por padrão, então se o usuário digitar a URL canActivate
, a página será redirecionada automaticamente para esta página. Além disso, dê uma olhada no AuthGuard
parâmetro: Aqui estamos criando uma referência para a classe left-panel.component.html
, 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. Lembre-se da seção de layout quando criamos o Dashboard Users
arquivo parecido com este?
our.site.url/users
É aqui que nosso código encontra a realidade. Agora podemos construir o código e testá-lo no URL: Você deve conseguir navegar da página do Painel para Usuários, mas o que acontece se você digitar o URL http://www.mysite.com/users/42
no navegador diretamente?
Observe que esse erro também aparece se você atualizar o navegador depois de já navegar com êxito 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 é um URL que especifica um caminho para um componente dentro do aplicativo. Por exemplo,
http://www.mysite.com/
é 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 esse URL de dentro de um cliente em execução. O roteador Angular interpreta a URL e roteia para aquela página e herói.
Mas clicar em um link em um e-mail, inseri-lo na barra de endereço 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 por essa URL, ignorando o roteador.Um servidor estático retorna rotineiramente index.html quando recebe uma solicitação de
http://www.mysite.com/users/42
. Mas rejeitasrc
e retorna um erro 404 - Não encontrado, a menos que esteja configurado para retornar index.html.
Para corrigir esse problema é muito simples, precisamos apenas criar a configuração do arquivo do provedor de serviços. Já que estou trabalhando com o IIS aqui, vou mostrar como fazer isso neste ambiente, mas o conceito é semelhante para o Apache ou qualquer outro servidor web.
Portanto, criamos um arquivo dentro de web.config
pasta chamada angular-cli.json
que se parece com isto:
{ '$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': {} } }
Então, precisamos ter certeza de que este ativo será copiado para a pasta implantada. Tudo o que precisamos fazer é alterar nosso arquivo de configurações Angular CLI AuthGuard
:
canActivateAuthGuard.ts
Você se lembra de como tivemos a aula helpers
implementado para definir a configuração de roteamento? Cada 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 é 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 { if (!this.helper.isAuthenticated()) { this.router.navigate(['/login']); return false; } return true; } }
—crie-o dentro de canActivate
pasta e tenha a seguinte aparência:
Router
Portanto, toda vez que mudamos a página, o método Helper
será chamado, o que verificará se o usuário está autenticado, caso contrário, utilizamos nosso helpers
instância para redirecionar para a página de login. Mas o que é esse novo método no helpers.ts
classe? Sob o localStorage
pasta vamos criar um arquivo localStorage
. Aqui, precisamos gerenciar sessionStorage
, onde armazenaremos o token que obtemos do back end.
Nota
Em relação a sessionStorage , você também pode usar cookies ou localStorage , e a decisão dependerá do comportamento que desejamos 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 recarregamentos de página. Se os dados que você está armazenando precisam estar disponíveis continuamente, então localStorage é preferível a import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs/Subject'; @Injectable() export class Helpers { private authenticationChanged = new Subject(); constructor() { } public isAuthenticated():boolean 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()); } } . Os cookies são principalmente para leitura do lado do servidor, enquanto Subject só pode ser lido do lado do cliente. Portanto, a questão é, em seu aplicativo, quem precisa desses dados --- o cliente ou o servidor? |
{ path: 'logout', component: LogoutComponent},
Nosso código de autenticação está fazendo sentido agora? Voltaremos ao localStorage
aula mais tarde, mas agora vamos voltar por um minuto para a configuração de roteamento. Dê uma olhada nesta linha:
components/login
Este é o nosso componente para sair do site e é apenas uma classe simples para limpar o logout.component.ts
. Vamos criá-lo em import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-logout', template:'' }) export class LogoutComponent implements OnInit { constructor(private router: Router, private helpers: Helpers) { } ngOnInit() { this.helpers.logout(); this.router.navigate(['/login']); } }
pasta com o nome de /logout
:
localStorage
Portanto, toda vez que acessamos o URL login.component.ts
, o 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']); }); } }
será removido e o site redirecionará para a página de login. Finalmente, vamos criar app.component.ts
como isso:
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(); } }
Como você pode ver, no momento, codificamos nossas credenciais aqui. Observe que aqui estamos chamando uma classe de serviço; criaremos essas classes de serviços para obter acesso ao nosso back end na próxima seção.
Finalmente, precisamos voltar ao Subject
arquivo, 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.
Observable
Lembre-se de Observable
classe em nossa classe auxiliar? Este é um authentication
. app.component.html
s fornecem suporte para a passagem de mensagens entre editores e assinantes em seu aplicativo. Cada vez que o token de autenticação muda, o Menu
propriedade será atualizada. Analisando o services
arquivo, provavelmente fará mais sentido agora:
token.service.ts
Neste ponto, estamos navegando para páginas diferentes, 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 de serviço classes em particular. Nosso primeiro atendimento será dentro do 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 { return this.http.post(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }
pasta, chamada TokenService
:
BaseService
A primeira chamada para o backend é 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 acontecerá se chamarmos outro ponto de extremidade? Como você pode ver aqui, 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); } }
(e classes de serviço em geral) herdam de super.header
classe. Vamos dar uma olhada nisso:
localStorage
Portanto, toda vez que fazemos uma chamada HTTP, implementamos o cabeçalho da solicitação usando apenas user.ts
. Se o token estiver em export class User { id: number; name: string; }
então ele será anexado dentro do cabeçalho, mas se não, iremos apenas definir o formato JSON. Outra coisa que podemos ver aqui é o que acontece se a autenticação falhar.
como criar um aplicativo para crianças
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 irá gerenciar o token e agora estamos prontos para obter a lista de usuários de nosso banco de dados.
Para obter dados do banco de dados, primeiro certifique-se de combinar as classes de modelo com os modelos de visualização de back-end em nossa resposta.
Em 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 { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }
E podemos criar agora o SeedAPI.Web.API
Arquivo:
Web.API
Bem-vindo à primeira etapa de nosso aplicativo Web API Core 2. A primeira coisa que precisamos é criar um ASP.Net Core Web Application, que chamaremos de ViewModels
.
Certifique-se de escolher o modelo vazio para um começo limpo, como você pode ver abaixo:
Isso é tudo, criamos a solução começando com um aplicativo da web vazio. Agora nossa arquitetura será como listamos abaixo, portanto, teremos que criar os diferentes projetos:
Para fazer isso, para cada um basta clicar com o botão direito na Solução e adicionar um projeto “Biblioteca de Classes (.NET Core)”.
Na seção anterior, criamos oito projetos, mas para que servem? Aqui está uma descrição simples de cada um:
Interfaces
: Este é o nosso projeto de inicialização e onde os terminais são criados. Aqui, configuraremos o JWT, as dependências de injeção e os controladores.Commons
: Aqui executamos conversões do tipo de dados que os controladores retornarão nas respostas para o front end. É uma boa prática combinar essas classes com os modelos front-end.Models
: Isso será útil na implementação de dependências de injeção. O benefício convincente de uma linguagem tipificada estaticamente é que o compilador pode ajudar a verificar se um contrato no qual seu código depende foi realmente cumprido.ViewModels
: Todos os comportamentos compartilhados e código de utilitário estarão aqui.Models
: É uma boa prática não combinar o banco de dados diretamente com o front-end Maps
, portanto, o objetivo de ViewModels
é criar classes de banco de dados de entidade independentes do front end. Isso nos permitirá, no futuro, alterar nosso banco de dados sem necessariamente ter um impacto em nosso front end. Também ajuda quando simplesmente queremos fazer alguma refatoração.Models
: Aqui é onde mapeamos Services
para Repositories
e vice versa. Esta etapa é chamada entre controladores e serviços.App_Start
: Uma biblioteca para armazenar toda a lógica de negócios.JwtTokenConfig.cs
: Este é o único lugar onde chamamos o banco de dados.As referências ficarão assim:
Nesta seção, veremos a configuração básica da autenticação de token e nos aprofundaremos no assunto de segurança.
Para começar a configurar o JSON web token (JWT), vamos criar a próxima classe dentro de 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(); }); } } }
pasta chamada appsettings.json
. O código interno será semelhante a este:
'Jwt': { 'Key': 'veryVerySecretKey', 'Issuer': 'http://localhost:50498/' }
Os valores dos parâmetros de validação dependerão da necessidade de cada projeto. O usuário e público válidos que podemos definir lendo o arquivo de configuração ConfigureServices
:
startup.cs
Então, precisamos apenas chamá-lo de // 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(); }
método em TokenController.cs
:
appsettings.json
Agora estamos prontos para criar nosso primeiro controlador chamado 'veryVerySecretKey'
. O valor que definimos em LoginViewModel
para ViewModels
deve corresponder ao que usamos para criar o token, mas primeiro, vamos criar o namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
dentro de nosso 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; } } }
projeto:
BuildToken
E, finalmente, o controlador:
Authenticate
O identityDbContext
método criará o token com o código de segurança fornecido. O Models
método apenas tem validação de usuário embutida no código no momento, mas precisaremos chamar o banco de dados para validá-lo no final.
Configurar o Entity Framework é realmente fácil, pois a Microsoft lançou a versão Core 2.0— EF Core 2 como diminutivo. Vamos nos aprofundar em um modelo code-first usando Context
, portanto, primeiro certifique-se de ter instalado todas as dependências. Você pode usar o NuGet para gerenciá-lo:
Usando o ApplicationContext.cs
projeto que podemos criar aqui dentro do IApplicationContext.cs
pasta dois arquivos, EntityBase
e EntityBase
. Além disso, precisaremos de um User.cs
classe.
O IdentityUser
os arquivos serão herdados por cada modelo de entidade, mas namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } }
é uma classe de identidade e a única entidade que herdará de 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().ToArray(); } } }
. Abaixo estão as duas classes:
ApplicationContext.cs
namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet Set() where T : class { return base.Set(); } 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(); } } } }
Agora estamos prontos para criar App_Start
, que terá a seguinte aparência:
Web.API
Estamos muito próximos, mas primeiro precisaremos criar mais classes, desta vez no 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(options => options.UseSqlServer(configuration.GetConnectionString('DefaultConnection'))); } } }
pasta localizada em 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' }); } } }
projeto. A primeira classe é inicializar o contexto do aplicativo e a segunda é criar dados de amostra apenas para fins de teste durante o desenvolvimento.
// 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(); }
App_Start
E nós os chamamos de nosso arquivo de inicialização:
DependencyInjectionConfig.cs
É uma boa prática usar injeção de dependência para mover entre projetos diferentes. Isso nos ajudará na comunicação entre controladores e mapeadores, mapeadores e serviços, e serviços e repositórios.
Dentro da pasta namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); } } }
vamos criar o arquivo Map
e ficará assim:
Service
Teremos de criar para cada nova entidade um novo Repository
, startup.cs
e // 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(); }
e combiná-los com este arquivo. Então, só precisamos chamá-lo de 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 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) { } } }
Arquivo:
Authorize
Finalmente, quando precisamos obter a lista de usuários do banco de dados, podemos criar um controlador usando esta injeção de dependência:
Map
Veja como o Maps
O atributo está presente aqui para garantir que o front end tenha efetuado login e como a injeção de dependência funciona no construtor da classe.
Finalmente, temos uma chamada para o banco de dados, mas primeiro, precisamos entender o ViewModels
projeto.
UserMap.cs
ProjetoEsta etapa serve apenas para mapear 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 GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List DomainToViewModel(List domain) { List model = new List(); 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; } } }
de e para modelos de banco de dados. Devemos criar um para cada entidade e, seguindo nosso exemplo anterior, o Services
arquivo será parecido com este:
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 GetAll() { return repository.GetAll(); } } }
Parece que mais uma vez, a injeção de dependência está funcionando no construtor da classe, vinculando Maps ao projeto Services.
Repositories
ProjetoNão há muito a dizer aqui: nosso exemplo é realmente simples e não temos lógica de negócios ou código para escrever aqui. Este projeto seria útil em requisitos avançados futuros quando precisamos calcular ou fazer alguma lógica antes ou depois das etapas do banco de dados ou do controlador. Seguindo o exemplo, a classe ficará bastante vazia:
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(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser(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); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } }
ProjetoEstamos chegando à última seção deste tutorial: Precisamos apenas fazer chamadas para o banco de dados, então criamos um
|_+_|arquivo onde podemos ler, inserir ou atualizar usuários no banco de dados.
|_+_|
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 JavaScript no front-end e o que pode competir com C # se você precisa do suporte do SQL Server e do Entity Framework no back-end? Portanto, a ideia deste artigo era combinar o melhor de dois mundos e espero que você tenha gostado.
Se você está trabalhando em uma equipe de Desenvolvedores angulares 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 poderia ser integrar o Swagger com a API Web 2. Swagger é uma ótima ferramenta para documentar e testar suas APIs RESTFul. Leia o guia da Microsoft: Comece com Swashbuckle e ASP.NET Core .
Se você ainda é muito novo no Angular 5 e está tendo problemas para acompanhar, leia Um tutorial do Angular 5: guia passo a passo para seu primeiro aplicativo Angular 5 pelo colega ApeeScapeer Sergey Moiseev.
Você pode usar qualquer IDE para o front-end; muitas pessoas gostam de juntar o front-end em uma biblioteca de aplicativos da Web e automatizar a implantação. Eu prefiro manter o código do front-end separado do back-end e descobri que o Visual Studio Code é uma ferramenta realmente boa, especialmente com o intellisense, para código Typescript.
Angular Material é uma estrutura de componente de IU, embora você não seja obrigado a usá-lo. Os frameworks de componentes de interface do usuário nos ajudam a organizar o layout e o responsivo no site, mas temos muitos no mercado, como bootstrap e outros, e podemos escolher a aparência que preferirmos.
O Roteador Angular permite a navegação de uma visão para a próxima conforme os usuários realizam tarefas de aplicativo. Ele pode interpretar a URL de um navegador como uma instrução para navegar até uma visualização gerada pelo cliente. O desenvolvedor tem permissão para configurar nomes de URL, parâmetros e contaminados com CanAuthenticate, podemos validar a autenticação do usuário
Freqüentemente, as classes precisam ter acesso umas às outras, e esse padrão de design demonstra como criar classes fracamente acopladas. Quando duas classes estão fortemente acopladas, elas são vinculadas a uma associação binária.
JSON Web Token (JWT) é uma biblioteca compacta para transmitir informações com segurança entre as partes como um objeto JSON. Os JWTs podem ser criptografados para fornecer sigilo entre as partes. Vamos nos concentrar nos tokens assinados.