Angular 5 y ASP.NET Core
Publicado: 2022-03-11He estado pensando en escribir una publicación de blog desde que la primera versión de Angular prácticamente mató a Microsoft en el lado del cliente. Tecnologías como ASP.Net, Web Forms y MVC Razor se han vuelto obsoletas, reemplazadas por un marco de JavaScript que no es exactamente Microsoft. Sin embargo, desde la segunda versión de Angular, Microsoft y Google han estado trabajando juntos para crear Angular 2, y fue entonces cuando mis dos tecnologías favoritas comenzaron a trabajar juntas.
En este blog, quiero ayudar a las personas a crear la mejor arquitectura combinando estos dos mundos. ¿Estás listo? ¡Aquí vamos!
Sobre la arquitectura
Construirá un cliente Angular 5 que consume un servicio RESTful Web API Core 2.
El lado del cliente:
- Angular 5
- CLI angular
- Material angular
El lado del servidor:
- .NET C# Web API Núcleo 2
- Dependencias de inyección
- autenticación JWT
- Código de marco de entidad primero
- servidor SQL
Nota
En esta publicación de blog, asumimos que el lector ya tiene conocimientos básicos de TypeScript, módulos Angular, componentes e importación/exportación. El objetivo de esta publicación es crear una buena arquitectura que permita que el código crezca con el tiempo. |
¿Qué necesitas?
Comencemos eligiendo el IDE. Por supuesto, esta es solo mi preferencia, y puedes usar la que te resulte más cómoda. En mi caso usaré Visual Studio Code y Visual Studio 2017.
¿Por qué dos IDE diferentes? Dado que Microsoft creó Visual Studio Code para el front-end, no puedo dejar de usar este IDE. De todos modos, también veremos cómo integrar Angular 5 dentro del proyecto de solución, eso lo ayudará si es el tipo de desarrollador que prefiere depurar tanto el back-end como el front-end con solo un F5.
En cuanto al backend, puedes instalar la última versión de Visual Studio 2017 que tiene una edición gratuita para desarrolladores pero es muy completa: Community.
Entonces, aquí la lista de cosas que necesitamos instalar para este tutorial:
- código de estudio visual
- Comunidad de Visual Studio 2017 (o cualquiera)
- Node.js v8.10.0
- Servidor SQL 2017
Nota
Verifique que esté ejecutando al menos Node 6.9.x y npm 3.xx ejecutando node -v y npm -v en una ventana de terminal o consola. Las versiones anteriores producen errores, pero las versiones más nuevas están bien. |
el frente
Inicio rápido
¡Que comience la fiesta! Lo primero que debemos hacer es instalar Angular CLI globalmente, así que abra el símbolo del sistema node.js y ejecute este comando:
npm install -g @angular/cli
Bien, ahora tenemos nuestro paquete de módulos. Esto generalmente instala el módulo en su carpeta de usuario. Un alias no debería ser necesario por defecto, pero si lo necesitas puedes ejecutar la siguiente línea:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
El siguiente paso es crear el nuevo proyecto. Lo llamaré angular5-app
. Primero, navegamos a la carpeta en la que queremos crear el sitio y luego:
ng new angular5-app
Primera compilación
Si bien puede probar su nuevo sitio web simplemente ejecutando ng serve --open
, le recomiendo probar el sitio desde su servicio web favorito. ¿Por qué? Bueno, algunos problemas solo pueden ocurrir en producción, y construir el sitio con ng build
es la forma más cercana de abordar este entorno. Luego, podemos abrir la carpeta angular5-app
con Visual Studio Code y ejecutar ng build
en la terminal bash:
Se creará una nueva carpeta llamada dist
y podemos servirla usando IIS o el servidor web que prefiera. Luego puedes escribir la URL en el navegador, y… ¡listo!
Nota
No es el propósito de este tutorial mostrar cómo configurar un servidor web, por lo que asumo que ya tiene ese conocimiento. |
La carpeta src
Mi carpeta src
está estructurada de la siguiente manera: dentro de la carpeta de la app
tenemos components
donde crearemos para cada componente Angular los archivos css
, ts
, spec
y html
. También crearemos una carpeta de config
para mantener la configuración del sitio, las directives
tendrán todas nuestras directivas personalizadas, los helpers
albergarán código común como el administrador de autenticación, el layout
contendrá los componentes principales como el cuerpo, la cabeza y los paneles laterales, los models
mantendrán lo que coincida con los modelos de vista de back-end y, finalmente, los services
tendrán el código para todas las llamadas al back-end.
Fuera de la carpeta de la app
, mantendremos las carpetas creadas por defecto, como assets
y environments
, y también los archivos raíz.
Creación del archivo de configuración
Vamos a crear un archivo config.ts
dentro de nuestra carpeta de config
y llamar a la clase AppConfig
. Aquí es donde podemos establecer todos los valores que usaremos en diferentes lugares de nuestro código; por ejemplo, la URL de la API. Tenga en cuenta que la clase implementa una propiedad get
que recibe, como parámetro, una estructura clave/valor y un método simple para obtener acceso al mismo valor. De esta manera, será fácil obtener los valores simplemente llamando a this.config.setting['PathAPI']
de las clases que heredan de él.
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 comenzar con el diseño, configuremos el marco del componente de la interfaz de usuario. Por supuesto, puedes usar otros como Bootstrap, pero si te gusta el estilo de Material, te lo recomiendo porque también es compatible con Google.
Para instalarlo, solo necesitamos ejecutar los siguientes tres comandos, que podemos ejecutar en la terminal de Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
El segundo comando se debe a que algunos componentes del material dependen de las animaciones angulares. También recomiendo leer la página oficial para comprender qué navegadores son compatibles y qué es un polyfill.
El tercer comando se debe a que algunos componentes de Material dependen de HammerJS para los gestos.
Ahora podemos proceder a importar los módulos de componentes que queremos usar en nuestro archivo 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 ],
El siguiente paso es cambiar el archivo style.css
, agregando el tipo de tema que desea usar:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
Ahora importe HammerJS agregando esta línea en el archivo main.ts
:
import 'hammerjs';
Y finalmente, todo lo que nos falta es agregar los íconos de Material a index.html
, dentro de la sección de encabezado:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
el diseño
En este ejemplo, crearemos un diseño simple como este:
La idea es abrir/ocultar el menú haciendo clic en algún botón en el encabezado. Angular Responsive hará el resto del trabajo por nosotros. Para ello crearemos una carpeta de layout
y pondremos dentro de ella los archivos app.component
creados por defecto. Pero también crearemos los mismos archivos para cada sección del diseño como puedes ver en la siguiente imagen. Luego, app.component
será el cuerpo, head.component
el encabezado y left-panel.component
el menú.
Ahora cambiemos app.component.html
de la siguiente manera:
<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>
Básicamente, tendremos una propiedad de authentication
en el componente que nos permitirá eliminar el encabezado y el menú si el usuario no ha iniciado sesión y, en su lugar, mostrar una página de inicio de sesión simple.
El head.component.html
se ve así:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
Solo un botón para cerrar la sesión del usuario; volveremos a esto más adelante. En cuanto a left-panel.component.html
, por ahora solo cambie el HTML a:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Lo hemos mantenido simple: hasta ahora son solo dos enlaces para navegar a través de dos páginas diferentes. (También volveremos a esto más adelante).
Ahora, así es como se ven el encabezado y los archivos TypeScript del componente del lado izquierdo:
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'; }
Pero, ¿qué pasa con el código TypeScript para app.component
? Dejaremos un pequeño misterio aquí y lo pausaremos por un tiempo, y volveremos a esto después de implementar la autenticación.
Enrutamiento
Bien, ahora tenemos material angular que nos ayuda con la interfaz de usuario y un diseño simple para comenzar a construir nuestras páginas. Pero, ¿cómo podemos navegar entre páginas?
Para crear un ejemplo simple, creemos dos páginas: "Usuario", donde podemos obtener una lista de los usuarios existentes en la base de datos, y "Panel", una página donde podemos mostrar algunas estadísticas.
Dentro de la carpeta de la app
, crearemos un archivo llamado app-routing.modules.ts
con este aspecto:
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 {}
Es así de simple: solo importando RouterModule
y Routes
desde @angular/router
, podemos mapear las rutas que queremos implementar. Aquí estamos creando cuatro caminos:
-
/dashboard
: Nuestra página de inicio -
/login
: La página donde el usuario puede autenticarse -
/logout
: una ruta simple para cerrar la sesión del usuario -
/users
: nuestra primera página donde queremos enumerar los usuarios desde el back-end
Tenga en cuenta que el dashboard
de control es nuestra página de forma predeterminada, por lo que si el usuario escribe la URL /
, la página se redirigirá automáticamente a esta página. Además, eche un vistazo al parámetro canActivate
: aquí estamos creando una referencia a la clase AuthGuard
, que nos permitirá comprobar si el usuario ha iniciado sesión. De lo contrario, se redirige a la página de inicio de sesión. En la siguiente sección, le mostraré cómo crear esta clase.
Ahora, todo lo que tenemos que hacer es crear el menú. ¿Recuerdas en la sección de diseño cuando creamos el archivo left-panel.component.html
para que se viera así?
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
Aquí es donde nuestro código se encuentra con la realidad. Ahora podemos compilar el código y probarlo en la URL: debería poder navegar desde la página del Panel de control hasta Usuarios, pero ¿qué sucede si escribe la URL our.site.url/users
en el navegador directamente?
Tenga en cuenta que este error también aparece si actualiza el navegador después de navegar con éxito a esa URL a través del panel lateral de la aplicación. Para comprender este error, permítanme consultar los documentos oficiales donde es muy claro:
Una aplicación enrutada debe admitir enlaces profundos. Un enlace profundo es una URL que especifica una ruta a un componente dentro de la aplicación. Por ejemplo,
http://www.mysite.com/users/42
es un enlace profundo a la página de detalles del héroe que muestra el héroe con id: 42.No hay problema cuando el usuario navega a esa URL desde un cliente en ejecución. El enrutador angular interpreta la URL y las rutas a esa página y héroe.
Pero hacer clic en un enlace en un correo electrónico, ingresarlo en la barra de direcciones del navegador o simplemente actualizar el navegador mientras se encuentra en la página de detalles del héroe: todas estas acciones son manejadas por el propio navegador, fuera de la aplicación en ejecución. El navegador realiza una solicitud directa al servidor para esa URL, sin pasar por el enrutador.Un servidor estático devuelve index.html de forma rutinaria cuando recibe una solicitud de
http://www.mysite.com/
. Pero rechazahttp://www.mysite.com/users/42
y devuelve un error 404 - Not Found a menos que esté configurado para devolver index.html en su lugar.
Solucionar este problema es muy simple, solo necesitamos crear la configuración del archivo del proveedor de servicios. Como estoy trabajando con IIS aquí, le mostraré cómo hacerlo en este entorno, pero el concepto es similar para Apache o cualquier otro servidor web.
Entonces creamos un archivo dentro de la carpeta src
llamado web.config
que se ve así:
<?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>
Luego, debemos asegurarnos de que este activo se copiará en la carpeta implementada. Todo lo que tenemos que hacer es cambiar nuestro archivo de configuración de 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": {} } }
Autenticación
¿Recuerdas cómo implementamos la clase AuthGuard
para establecer la configuración de enrutamiento? Cada vez que naveguemos a una página diferente usaremos esta clase para verificar si el usuario está autenticado con un token. De lo contrario, lo redirigiremos automáticamente a la página de inicio de sesión. El archivo para esto es canActivateAuthGuard.ts
; créelo dentro de la carpeta de helpers
y haga que se vea así:
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; } }
Entonces, cada vez que cambiemos la página, se llamará al método canActivate
, que verificará si el usuario está autenticado y, de lo contrario, usaremos nuestra instancia de Router
para redirigir a la página de inicio de sesión. Pero, ¿qué es este nuevo método en la clase Helper
? En la carpeta de helpers
, creemos un archivo helpers.ts
. Aquí necesitamos administrar localStorage
, donde almacenaremos el token que obtengamos del back-end.
Nota
Respecto a localStorage , también se pueden utilizar cookies o sessionStorage , y la decisión dependerá del comportamiento que queramos implementar. Como sugiere el nombre, sessionStorage solo está disponible durante la sesión del navegador y se elimina cuando se cierra la pestaña o la ventana; sin embargo, sobrevive a las recargas de página. Si los datos que está almacenando deben estar disponibles de forma continua, entonces es preferible localStorage a sessionStorage . Las cookies son principalmente para leer en el lado del servidor, mientras que localStorage solo se puede leer en el lado del cliente. Entonces, la pregunta es, en su aplicación, ¿quién necesita estos datos, el cliente o el 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()); } }
¿Tiene sentido nuestro código de autenticación ahora? Volveremos a la clase Subject
más tarde, pero ahora regresemos por un minuto a la configuración de enrutamiento. Echa un vistazo a esta línea:
{ path: 'logout', component: LogoutComponent},
Este es nuestro componente para cerrar sesión en el sitio, y es solo una clase simple para limpiar localStorage
. Vamos a crearlo en la carpeta components/login
con el nombre 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']); } }
Entonces, cada vez que vayamos a la URL /logout
, se eliminará localStorage
y el sitio se redirigirá a la página de inicio de sesión. Finalmente, login.component.ts
así:
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 puede ver, por el momento hemos codificado nuestras credenciales aquí. Tenga en cuenta que aquí estamos llamando a una clase de servicio; crearemos estas clases de servicios para obtener acceso a nuestro back-end en la siguiente sección.

Finalmente, debemos volver al archivo app.component.ts
, el diseño del sitio. Aquí, si el usuario está autenticado, mostrará las secciones de menú y encabezado, pero si no, el diseño cambiará para mostrar solo nuestra página de inicio de sesión.
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(); } }
¿Recuerdas la clase Subject
en nuestra clase auxiliar? Este es un Observable
. Observable
s brinda soporte para pasar mensajes entre editores y suscriptores en su aplicación. Cada vez que cambie el token de authentication
, se actualizará la propiedad de autenticación. Revisando el archivo app.component.html
, probablemente tendrá más sentido ahora:
<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>
Servicios
En este punto, estamos navegando a diferentes páginas, autenticando nuestro lado del cliente y representando un diseño muy simple. Pero, ¿cómo podemos obtener datos del back-end? Recomiendo encarecidamente hacer todo el acceso de back-end desde las clases de servicio en particular. Nuestro primer servicio estará dentro de la carpeta de services
, llamada 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) ); } }
La primera llamada al back-end es una llamada POST a la API del token. La API de token no necesita la cadena de token en el encabezado, pero ¿qué sucede si llamamos a otro punto final? Como puede ver aquí, TokenService
(y las clases de servicio en general) heredan de la clase BaseService
. Echemos un vistazo a esto:
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); } }
Entonces, cada vez que hacemos una llamada HTTP, implementamos el encabezado de la solicitud simplemente usando super.header
. Si el token está en localStorage
, se agregará dentro del encabezado, pero si no, simplemente estableceremos el formato JSON. Otra cosa que podemos ver aquí es lo que sucede si falla la autenticación.
El componente de inicio de sesión llamará a la clase de servicio y la clase de servicio llamará al back-end. Una vez que tengamos el token, la clase auxiliar administrará el token y ahora estamos listos para obtener la lista de usuarios de nuestra base de datos.
Para obtener datos de la base de datos, primero asegúrese de hacer coincidir las clases de modelo con los modelos de vista de back-end en nuestra respuesta.
En user.ts
:
export class User { id: number; name: string; }
Y podemos crear ahora el archivo 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)); }
la parte de atrás
Inicio rápido
Bienvenido al primer paso de nuestra aplicación Web API Core 2. Lo primero que necesitamos es crear una aplicación web ASP.Net Core, a la que llamaremos SeedAPI.Web.API
.
Asegúrese de elegir la plantilla vacía para un comienzo limpio como puede ver a continuación:
Eso es todo, creamos la solución a partir de una aplicación web vacía. Ahora nuestra arquitectura será como la listamos a continuación por lo que tendremos que crear los diferentes proyectos:
Para hacer esto, para cada uno, simplemente haga clic con el botón derecho en la Solución y agregue un proyecto de "Biblioteca de clases (.NET Core)".
La arquitectura
En el apartado anterior creamos ocho proyectos, pero ¿para qué sirven? Aquí hay una descripción simple de cada uno:
-
Web.API
: este es nuestro proyecto de inicio y donde se crean los puntos finales. Aquí configuraremos JWT, dependencias de inyección y controladores. -
ViewModels
: aquí realizamos conversiones del tipo de datos que los controladores devolverán en las respuestas al front-end. Es una buena práctica hacer coincidir estas clases con los modelos front-end. -
Interfaces
: esto será útil para implementar dependencias de inyección. El beneficio convincente de un lenguaje tipificado estáticamente es que el compilador puede ayudar a verificar que realmente se cumpla un contrato en el que se basa su código. -
Commons
: todos los comportamientos compartidos y el código de utilidad estarán aquí. -
Models
: es una buena práctica no hacer coincidir la base de datos directamente con losViewModels
, por lo que el propósito de losModels
es crear clases de bases de datos de entidades independientes del frontal. Eso nos permitirá en el futuro cambiar nuestra base de datos sin tener necesariamente un impacto en nuestra interfaz. También ayuda cuando simplemente queremos hacer algo de refactorización. -
Maps
: aquí es dondeViewModels
aModels
y viceversa. Este paso se llama entre controladores y Servicios. -
Services
: Una biblioteca para almacenar toda la lógica de negocios. -
Repositories
: Este es el único lugar donde llamamos a la base de datos.
Las referencias se verán así:
Autenticación basada en JWT
En esta sección veremos la configuración básica de la autenticación por token y profundizaremos un poco más en el tema de la seguridad.
Para comenzar a configurar el token web JSON (JWT), creemos la siguiente clase dentro de la carpeta App_Start
llamada JwtTokenConfig.cs
. El código interior se verá así:
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(); }); } } }
Los valores de los parámetros de validación dependerán del requerimiento de cada proyecto. El usuario y la audiencia válidos que podemos configurar leyendo el archivo de configuración appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
Entonces solo necesitamos llamarlo desde el método ConfigureServices
en 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(); }
Ahora estamos listos para crear nuestro primer controlador llamado TokenController.cs
. El valor que establecemos en appsettings.json
en "veryVerySecretKey"
debe coincidir con el que usamos para crear el token, pero primero, creemos LoginViewModel
dentro de nuestro proyecto ViewModels
:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
Y finalmente el 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; } } }
El método BuildToken
creará el token con el código de seguridad proporcionado. El método de Authenticate
solo tiene la validación del usuario codificada por el momento, pero necesitaremos llamar a la base de datos para validarla al final.
El contexto de la aplicación
Configurar Entity Framework es realmente fácil desde que Microsoft lanzó la versión Core 2.0, EF Core 2 para abreviar. Vamos a profundizar con un modelo basado en el código primero usando identityDbContext
, así que primero asegúrese de haber instalado todas las dependencias. Puede usar NuGet para administrarlo:
Usando el proyecto Models
podemos crear aquí dentro de la carpeta Context
dos archivos, ApplicationContext.cs
y IApplicationContext.cs
. Además, necesitaremos una clase EntityBase
.
Cada modelo de entidad heredará los archivos EntityBase
, pero User.cs
es una clase de identidad y la única entidad que heredará de IdentityUser
. A continuación se muestran ambas clases:
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(); } } }
Ahora estamos listos para crear ApplicationContext.cs
, que se verá así:
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 muy cerca, pero primero necesitaremos crear más clases, esta vez en la carpeta App_Start
ubicada en el proyecto Web.API
. La primera clase es para inicializar el contexto de la aplicación y la segunda es para crear datos de muestra solo con el propósito de probar durante el desarrollo.
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(); }
Inyección de dependencia
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.
El proyecto de Maps
Este paso es solo para mapear ViewModels
hacia y desde modelos de bases de datos. Debemos crear uno para cada entidad y, siguiendo nuestro ejemplo anterior, el archivo UserMap.cs
se verá así:
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 una vez más, la inyección de dependencia está funcionando en el constructor de la clase, vinculando Maps con el proyecto de Servicios.
El Proyecto Services
No hay mucho que decir aquí: nuestro ejemplo es realmente simple y no tenemos lógica comercial o código para escribir aquí. Este proyecto sería útil en futuros requisitos avanzados cuando necesitemos calcular o hacer algo de lógica antes o después de los pasos de la base de datos o del controlador. Siguiendo el ejemplo, la clase se verá bastante simple:
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(); } } }
El proyecto de Repositories
Estamos llegando a la última sección de este tutorial: solo necesitamos hacer llamadas a la base de datos, por lo que creamos un archivo UserRepository.cs
donde podemos leer, insertar o actualizar usuarios en la base de datos.
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; } } } }
Resumen
En este artículo, expliqué cómo crear una buena arquitectura usando Angular 5 y Web API Core 2. En este punto, ha creado la base para un gran proyecto con código que soporta un gran crecimiento en los requisitos.
La verdad es que nada compite con JavaScript en el front-end y ¿qué puede competir con C# si necesita el soporte de SQL Server y Entity Framework en el back-end? Así que la idea de este artículo fue combinar lo mejor de dos mundos y espero que lo hayas disfrutado.
¿Que sigue?
Si está trabajando en un equipo de desarrolladores de Angular, probablemente podría haber diferentes desarrolladores trabajando en el front-end y en el back-end, por lo que una buena idea para sincronizar los esfuerzos de ambos equipos podría ser integrar Swagger con Web API 2. Swagger es una gran herramienta para documentar y probar sus API RESTFul. Lea la guía de Microsoft: Introducción a Swashbuckle y ASP.NET Core.
Si todavía es muy nuevo en Angular 5 y tiene problemas para seguirlo, lea Un tutorial de Angular 5: Guía paso a paso para su primera aplicación Angular 5 por el compañero Toptaler Sergey Moiseev.