Elpequeñolibrodeaspnetcore 13032019
Elpequeñolibrodeaspnetcore 13032019
Elpequeñolibrodeaspnetcore 13032019
Introduction 1.1
Tú primera aplicación 1.2
2
Seguridad e Identidad 1.7
3
El pequeño libro de ASP.NET Core
Por Nate Barbettini
ISBN: 978-1-387-75615-5
Introducción
¡Gracias por elegir El pequeño libro de ASP.NET Core! Escribí este corto
libro para ayudar a los desarrolladores y personas interesadas en aprender
programación web a aprender acerca de ASP.NET Core 2.0, un marco de
trabajo para construir aplicaciones web y APIs.
4
Cómo agregar inicio de sesión, registro y seguridad
Cómo desplegar la aplicación en la web
Antes de empezar
El código de la versión final de la aplicación que construirás está disponible
en Github:
https://www.github.com/nbarbettini/little-aspnetcore-todo
6
ASP.NET Core se ejecuta sobre el entorno de ejecución .NET de Microsoft,
similar a la Máquina Virtual de Java (JVM) o el intérprete de Ruby. Puedes
escribir aplicaciones ASP.NET Core en un número de lenguajes (C#, Visual
Basic y F#). C# es la elección más popular, y es lo que usaremos en este
libro. Puedes construir y ejecutar aplicaciones ASP.NET Core en Windows,
Mac y Linux.
7
cruzados(CSRF), así no tienes que hacerlo tú mismo. También tienes el
beneficio de tipado estático con el compilador .NET, el cuál es como
tener un linter muy paranoico encendido todo el tiempo. Algunas veces
esto hace más difícil hacer cosas no intencionado una variable o
pedazos de datos.
Es posible que también hayas escuchado acerca del .NET Core y Estándar
.NET. Estas son términos que se confunden así aquí esta una simple
explicación:
8
Y solo como una buena medida, .NET Framework es una implementación
diferente del estándar .NET que es específica para Windows. Este era el
único entorno de ejecución .NET hasta que .NET Core llego y proporciono
.NET para Mac y Linux. ASP.NET Core puede correr sobre el .NET
Framework solo Windows, pero no tratare sobre esto demasiado.
9
10
Tú primera aplicación
¿Estas listo para crear tú primera aplicación web con ASP.NET Core?
Primero, tendrás que conseguir algunas cosas:
11
Consigue el SDK
Para obtener el SDK de .NET Core busca "descargar .NET Core" y sigue las
instrucciones de la página de descargas de Microsoft. Después de que el
SDK ha finalizado de instalarse, abre una Terminal (o PowerShell en
Windows) y usa la herramienta de linea de comando dotnet (también
conocida como CLI) para asegurarte que todo está funcionando:
dotnet --version
2.1.104
info :
dotnet --info
Product Information:
Version: 2.1.104
Commit SHA-1 hash: 48ec687460
Runtime Environment:
OS Name: Mac OS X
OS Version: 10.13
(más detalles...)
12
13
Hola Mundo en C#
Antes de profundizar en ASP.NET Core, prueba creando y ejecutando una
aplicación de C# simple.
Puedes hacer todo esto desde la línea de comandos. Primero, abre una
Terminal (o PowerShell en Windows). Navega a la ubicación donde deseas
guardar tus proyectos, tal cómo la carpeta de mis Documentos:
cd Documentos
cd CsharpHelloWorld
14
CsharpHelloWorld.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
</PropertyGroup>
</Project>
Program.cs
using System;
namespace CsharpHelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
15
Dentro de la carpeta del proyecto, usa dotnet run para ejecutar el
programa. Veras que la salida se escribe en la consola después que el código
compila:
dotnet run
Hello World!
16
Crear un proyecto de ASP.NET
Core
Si todavía estas en el directorio creado para la aplicación Hello World,
muévete a tu directorio Documentos o directorio inicial:
cd ..
mkdir AspNetCoreTodo
cd AspNetCoreTodo
A continuación, crea un nuevo proyecto con dotnet new , esta vez utilizaras
opciones adicionales:
17
solución. Después, agregaras más directorios de proyecto junto al
directorio de proyecto AspNetCoreTodo. Todos en una solo
directorio de la solución.
Verás unos pocos archivos en la carpeta del nuevo proyecto, Una vez que
abres el nuevo directorio, todo lo que tienes que hacer para ejecutar el
proyecto es:
dotnet run
18
brindar cosas como archivos estáticos o páginas de errores. también en
donde agregas tus propios servicios al contenedor de inyección de
dependencias (posteriormente habrá más sobre esto).
19
Foco para corregir problemas: Si tu código contiene lineas rojos
(errores del compilador, coloca el cursor sobre el código que esta en
rojo y mirar el icono del foco encendido en el margen izquierdo. el foco
te sugerirá reparaciones comunes, como agregar enunciados using
faltantes en tu código:
Shift-B para ejecutar la tarea de Build run la cual realiza lo mismo que
dotnet build .
20
Una nota acerca de Git
Si usar Git o Github para manejar el control de código fuente, ahora es buen
momento para hacer un git init e iniciar el repositorio en el directorio
raíz del proyecto:
cd ..
git init
Asegúrate que agregues un archivo .gitignore que ignora las carpeta bin
Hay mucho más que explorar, así que profundicemos e iniciemos a construir
una aplicación.
21
Fundamentos de MVC
En este capitulo, explorarás el patrón MVC en ASP.NET Core. MVC
(Modelo-Vista-Controlador) es un patrón para construir aplicaciones web
que es usado en casi todos los marcos de desarollo web (ejemplos populares
son Ruby on Rails y Express), adicionalmente marcos de trabajo del lado de
cliente con Javascript como Angular. Las aplicaciones móviles sobre iOS y
Android también usan una variante de MVC.
22
¿Qué es lo que contruirás?
El ejercicio de "Hola Mundo" de MVC es construir una aplicación de lista
de tareas. Es un proyecto genial ya que es pequeño y simple en alcance, pero
trata cada una de las partes de MVC y cubre muchos conceptos que usaras
en un aplicación más grande.
23
Crear un controlador
Actualmente ya hay algunos controladores en la carpeta Controllers,
incluyendo HomeController que generá la pantalla de bienvenida por defautl
cuando visitas http://localhost:5000 . Puedes ignorar estos controladores
por ahora.
Controllers/TodoController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace AspNetCoreTodo.Controllers
{
public class TodoController : Controller
{
// Las acciones van aquí
}
}
Las rutas que son manejadas por el controlador son llamadas acciones, y son
representadas por métodos en la clase controlador. Por ejemplo, el
HomeController incluye tres métodos de acción ( Index , About ,y
Contact ) las cuales son mapeadas por ASP.NET Core a estas rutas URLs:
24
Hay un gran número de convenciones (patrones comunes) usados por
ASP.NET Core, tales como patrón que FooController se convierte en
/Foo , y la acción Index puede ser omitida de la URL. Puedes personalizar
este comportamiento si así lo deseas, pero por ahora, usaremos las
convenciones predefinidas.
Antes de que puedas escribir el resto del código del controlador, necesitas
crear un modelo y una vista.
25
26
Crear modelos
Hay dos clases modelo diferentes que necesitan ser creadas: un modelo que
representa las tareas almacenadas en la base de datos (a veces llamadas
entidades), y el modelo que será combinado con la vista (MV en MVC) y
sera enviado al navegador del usuario. Debido a con ambos son referidos
como modelos, prefiero referirme al ultimo como View Model.
Models/TodoItem.cs
using System;
using System.ComponentModel.DataAnnotations;
namespace AspNetCoreTodo.Models
{
public class TodoItem
{
public Guid Id { get; set; }
[Required]
public string Title { get; set; }
Esta clase define lo que base de datos necesitara para almacenar cada tarea
:Un ID, un titulo o nombre,si la tarea esta completada y la fecha de termino.
Cada linea define una propiedad de la clase:
27
La propiedad Id es un GUID o a Identificador Global Único. Los Guid
son cadenas largas de letras y números como por ejemplo, 43ec09f2-
28
Cada propiedad es seguida por get; set; , las cuales es una forma corta de
decir que la propiedad es de lectura/escritura (más técnicamente que tiene
métodos modificadores.)
La vista modelo
Frecuentemente, el modelo que almacenas en la base de datos es similar
pero no exactamente el mismo que deseas usar en la MVC (la vista modelo).
En este caso, el modelo TodoItem representa a un único elemento de la base
de datos. Pero la vista puede necesitar mostrar dos, diez o cientos tareas
pendientes (dependiendo que tal malamente el usuario esta procrastinando).
Debido a esto, la vista modelo puede ser una clase separada que mantienen
un arreglo de TodoItem
Models/TodoViewModel.cs
namespace AspNetCoreTodo.Models
{
public class TodoViewModel
{
public TodoItem[] Items { get; set; }
}
}
29
Ahora que tienes algunos modelos, es tiempo de crear una vista que usa un
TodoViewModel y generará el HTML correcto para mostrar al usuario su lista
de tareas pendientes.
30
Crear una vista
Las vistas en ASP.NET Core se construyen usando el lenguaje de plantillas
Razor, el cual combina HTML y código C#. Si haz escrito páginas usan
Handlebar mustaches, ERM en Ruby On Rail or Thumeleaf en Java, ya
tienes la idea básico)
Las vista generada por la acción Index del "" necesita obtener datos en el
View Model, (una sección de todo items u mostrando en un bonita tabla para
el usuario, Por convención, las vistas va colocadas en la carpeta Views en un
subcarperta correspondiente al nombre del controlador. El nombre del
archivo es el nombre de la acción a con un una extensión .cshtml ,
Crea una carpeta llamada Todo dentro la carpeta Views , y agrega este
archivo: Views/Todo/Index.cshtml
@model TodoViewModel
@{
ViewData["Title"] = "Manage your todo list";
}
31
<td>Due</td>
</tr>
</thead>
El archivo de diseño
Quizás se pregunte dónde está el resto del HTML: ¿qué pasa con la etiqueta
<body> o el encabezado y pie de página de la página? ASP.NET Core
utiliza una vista de diseño que define la estructura base en la que se procesan
32
todas las demás vistas. Esta almacenado en Views/Shared/_Layout.cshtml .
wwwroot/css/site.css
div.todo-panel {
margin-top: 15px;
}
table tr.done {
text-decoration: line-through;
color: #888;
}
Puedes usar reglas CSS como estas para personalizar como se visualizan y
lucen tus páginas.
ASP.NET Core y Razor pueden hacer mucho más, como vistas parciales y
vistas generadas en el servidor componentes, pero un simple Layout y una
vista es todo lo que necesitaras por ahora. La documentación oficial(en
https://docs.asp.net de ASP.NET Core contiene muchos ejemplos si deseas
aprender más.
33
34
Agregar una clase de servicio
Haz creado un modelo, una vista y un controlador. Antes de usar el modelo
y la vista en el controlador, también necesitas escribir código que obtendrá
la lista de tareas de los usuarios desde la base de datos.
Una vez más es posible hacer todas estas cosas en un solo y enorme
controlador, que rápidamente se convertiría en difícil de manejar y probar.
En lugar es común ver aplicaciones dividen en dos o tres o más capas y tiers
de tal forma que cada una maneja uno (y solo una) aspecto de la aplicación.
Esto ayuda a mantener el controlador tan simple como sea posible, y hace
más fácil probar y cambiar la lógica de negocio y el código de acceso a base
de datos después.
35
Separado tu aplicación en esta forma es a veces llamada mult-tier o n-tier
arquitectura. En algunos casos los tiers o capas son proyectos
completamente separados. Pero otras veces i solo se referencia a como las
clases son organizadas y utilizadas. Lo más importante es pensar a cerca de
como dividir tu aplicación en piezas manejables y evitar tener controladores
o clases enormes que intentan hacer todo.
Para este proyecto, usaras dos capa de aplicación: una capa de presentación
compuesta de controladores y vistas que interactúan con el usuario, y una
capa de servicio que combina las reglas del negocio con el código de
accesos a base de datos. La capa de presentación ya existe asi que el
siguiente paso es construir un servicio que maneja las reglas de negocio para
las tareas y las guarda en una base da datos.
36
Por convención, las el nombre de las interfaces tiene el prefijo "I". Crea un
nuevo archivo en el directorio Services:
Services/ITodoItemService.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNetCoreTodo.Models;
namespace AspNetCoreTodo.Services
{
public interface ITodoItemService
{
Task<TodoItem[]> GetIncompleteItemsAsync();
}
}
The type or namespace name 'TodoItem' could not be found (are you mis
sing a using directive or an assembly reference?)
37
Debido a que esta es una interfaces, no hay ningún código aquí, solo la
definición (o la firma del método) GetIncompleteItemsAsync . Este método
no requiere parámetros y regresa un objeto del tipo Task<TodoItem[]> .
Services/FakeTodoItemService.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using AspNetCoreTodo.Models;
namespace AspNetCoreTodo.Services
{
public class FakeTodoItemService : ITodoItemService
{
public Task<TodoItem[]> GetIncompleteItemsAsync()
{
var item1 = new TodoItem
{
Title = "Learn ASP.NET Core",
DueAt = DateTimeOffset.Now.AddDays(1)
38
};
pero siempre regresa el mismo arreglo de dos TodoItem . Usaras esta para
poblar el controlador y la vista y después agregaras código de bases de datos
real en Usando una base de datos.
39
Usar inyección de dependencias
Regresa al controlador TodoController , añade algo de código para trabajar
con el servicio ITodoItemService :
using AspNetCoreTodo.Services;
40
La línea public TodoController(ITodoItemService todoItemService) define
un constructor para la clase. El constructor es un método especial que es
invocado cada que deseas crear una nueva instancia de la clase (en este caso
la clase TodoController ). Por haber agregado un parámetro
ITodoItemService al constructor, haz declarado que para crear
TodoController necesitar proveer un objeto que implementa la interfaz
ITodoItemService .
// ...
}
41
El patrón de Task es común cuando tu código realiza llamada a la base de
datos o una API de servicio, porque no sara capaz de regresar un resultado
real hasta que la base de datos (o red) responda.Si haz usado promesas o
callbacks en Javascript o otro lenguaje, Task es la misma idea: la promesa
que habrá un resultado - en algún tiempo futuro.
42
Ya casi terminamos. Has hecho que el TodoController dependa de la
interface ITodoItemService , pero aun no le has dicho a ASP.NET Core que
tu deseas el FakeTodoItemService sea el servicio actual que use debajo del
capo. Parecerá obvio ahora debido a que solo existe una clase que
implementa la interfaz ITodoItemService , pero después tendrás múltiples
clases que implementan la misma interface, asi que ser explicito es
necesario.
Declarando (o conectando) cual clase concreta para usar para cada interface
se hace en el método ConfigureServices de la clase Startup . Ahora mismo
algo como esto:
Startup.cs
services.AddMvc();
}
services.AddSingleton<ITodoItemService, FakeTodoItemService>();
43
Esta linea le especifica a ASP.NET Core que cada que se solicite
ITodoItemService en un constructor deberá usar la clase
FakeTodoItemService .
44
Finalizando el controlador
El último paso es finalizar el código del controlador . El controlador ahora
tiene un lista de tareas de la capa de servicio, y necesita poner que los items
dentro de un TodoViewModel y enlazar este modelo a la vista creada
anteriormente:
Controllers/TodoController.cs
return View(model);
}
Si no lo haz hecho ya, asegúrate que los siguientes enunciados using estén
en la parte superior del archivo:
using AspNetCoreTodo.Services;
using AspNetCoreTodo.Models;
Probando
45
Para iniciar la aplicación presiona F5 (si estas usando Visual Studio o Visual
Studio Code), o solo teclea dotnet run en la terminal. Si el código compila
sin errores, el servidor empezara escuchando en el puerto 5000 de forma
predeterminada. Si tu navegador navegador no se abre de forma automática,
ábrelo y navega a la dirección http://localhost:5000/todo. Verás la vista que
creaste, con los datos ficticios (por ahora) obtenidos de la base de datos .
46
Actualizar el layout
El archivo de layout en Views/Shared/_Layout.cshtml contiene el código
HTML base para cada vista. Este incluye la barra de navegación, la cual es
generada en la parte superior de cada página.
Views/Shared/_Layout.cshtml
<li>
<a asp-controller="Todo" asp-action="Index">My to-dos</a>
</li>
47
ruta es genera y agregada al elemento a como un atributo href, Esto significa
que el código hardcode la ruta cha el controlador . Es liage de eso ASP.NET
Core la genera por ti de forma automática.
48
Agregar paquetes externos
Una de las grandes ventajas de usar un ecosistema maduro como .NET es
que el número de paquetes y plugins de terceros es enorme. Al igual que
otros sistemas de paquetes, puedes descargar e instalar paquetes .NET que te
ayudaran con casi cualquier tarea o problema que puedas imaginar.
Puedes escribir código por ti mismo para convertir una fecha en formato
ISO 8601 en una cadena amigable para humanos, pero afortunadamente, hay
una manera mucho más rápida.
49
El paquete Humanizer en NuGet soluciona este problema proporcionado
métodos que pueden "humanizar" o reescribir casi cualquier cosa: fechas,
horas, duraciones, números y así sucesivamente. Es un proyecto open source
fantástico y útil que es publicado bajo la licencia permisiva MIT.
Debido a que Humanizar será usado para reescribir las fechas mostradas en
la vista, puedes usarlo directamente en las vistas misma. Primero añade un
enunciado @using al principio de la vista.
Views/Todo/Index.cshtml
@model TodoViewModel
@using Humanizer
// ...
<td>@item.DueAt.Humanize()</td>
50
Ahora las fechas son mucho más legibles.
Hay paquetes disponibles en NuGet para todo desde parsear un XML hasta
aprendizaje automático para postear en Twitter. ASP.NET Core mismo, bajo
el capo, no es más que una colección de paquetes de NuGet que son
agregados a tu proyecto.
El archivo de proyecto creado por dotnet new mvc incluye una sola
referencia al paquete Microsoft.AspNetCore.All que es un
"metapaquete" conveniente que hace referencia a todos los otros
paquetes de ASP.NET Core que necesitas para un proyecto típico. De
esta forma no tienes que tener cientos de referencias a paquetes en tu
archivo de proyecto.
51
Usar una base de datos
Escribir código de acceso a base de datos puede ser complicado. Es una
mala idea pegar consultas de SQL en el código de su aplicación, a menos
que realmente sepa lo que está haciendo. Un mapeador de objetos
relacional (ORM) facilita la escritura de código que interactúa con una base
de datos agregando una capa de abstracción entre su código y la base de
datos en sí. Hibernate para Java y ActiveRecord para Ruby son dos ORM
bien conocidos.
Existen varios ORM para .NET, incluido uno creado por Microsoft e
incluido en ASP.NET Core de forma predeterminada: Entity Framework
Core. Entity Framework Core facilita la conexión a varios tipos de bases de
datos diferentes y le permite utilizar el código C# para crear consultas de
base de datos que se asignan nuevamente a los modelos C# (POCO Plain
Old CLR Objects).
52
53
Conectarse a una base de datos
Hay algunas cosas que necesita para usar Entity Framework Core para
conectarse a una base de datos. Ya que usó dotnet new y la plantilla MVC
+ Individual Auth para configurar su proyecto, ya los tiene:
54
proveedor de base de datos deben utilizar en el método ConfigureServices
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")));
Como puedes ver, dotnet new ¡crea muchas cosas por ti! La base de datos
está configurada y lista para ser utilizada. Sin embargo, no tiene tablas para
almacenar elementos de tareas pendientes. Para almacenar sus entidades
TodoItem , necesitará actualizar el contexto y migrar la base de datos.
55
Actualizar el contexto
Todavía no hay mucho que hacer en el contexto de la base de datos:
Data/ApplicationDbContext.cs
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
// ...
56
Un DbSet representa una tabla o colección en la base de datos. Al crear una
propiedad DbSet<TodoItem> llamada Items , le está diciendo a Entity
Framework Core que desea almacenar las entidades TodoItem en una tabla
llamada Items .
Para actualizar la base de datos para reflejar el cambio que acaba de realizar
en el contexto, debe crear una migración.
57
Crear una migración
Las migraciones hacen un seguimiento de los cambios en la estructura de la
base de datos a lo largo del tiempo. Permiten deshacer (revertir) un conjunto
de cambios o crear una segunda base de datos con la misma estructura que la
primera. Con las migraciones, tiene un historial completo de modificaciones,
como agregar o eliminar columnas (y tablas completas).
58
El primer archivo de migración (con un nombre como
00_CreateIdentitySchema.cs ) se creó y se aplicó hace mucho cuando ejecutó
dotnet new . La nueva migración de AddItem tiene el prefijo de una marca
de tiempo cuando la creas.
Data/Migrations/_AddItems.cs
migrationBuilder.CreateTable(
name: "Items",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
DueAt = table.Column<DateTimeOffset>(nullable: true),
IsDone = table.Column<bool>(nullable: false),
Title = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Items", x => x.Id);
});
59
// (some code...)
}
migrationBuilder.DropTable(
name: "Items");
// (some code...)
}
en el método Down .
Si usa una base de datos SQL completa, como SQL Server o MySQL, esto
no será un problema y no tendrá que hacer esta solución (la cual es
ciertamente hacker).
60
Aplicar la migración
El último paso después de crear una (o más) migraciones es aplicarlas
realmente a la base de datos:
Este comando hará que Entity Framework Core cree la tabla Items en la
base de datos.
¡Eso es! Tanto la base de datos como el contexto están listos para funcionar.
A continuación, utilizará el contexto en su capa de servicio.
61
Crear una nueva clase de servicio
Anteriormente en el capítulo de conceptos básicos de MVC, creaste un
FakeTodoItemService que contenía elementos de tareas pendientes
codificados. Ahora que tiene un contexto de base de datos, puede crear una
nueva clase de servicio que usará Entity Framework Core para obtener los
tareas reales de la base de datos.
Services/TodoItemService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using AspNetCoreTodo.Data;
using AspNetCoreTodo.Models;
using Microsoft.EntityFrameworkCore;
namespace AspNetCoreTodo.Services
{
public class TodoItemService : ITodoItemService
{
private readonly ApplicationDbContext _context;
62
}
}
}
Luego, el método Where se usa para filtrar solo las tareas que no están
completas:
63
Para que el método sea un poco más corto, puedes eliminar la variable
intermedia items y simplemente devolver el resultado de la consulta
directamente (que hace lo mismo):
services.AddScoped<ITodoItemService, TodoItemService>();
64
¡El TodoController que depende de un ITodoItemService inyectado será
maravillosamente inconsciente del cambio en las clases de servicios, pero
bajo el capó estará usando Entity Framework Core y¡hablando con una base
de datos real!
Pruébalo
Inicie la aplicación y navegue a http://localhost:5000/todo . Los tareas
falsos se han ido y su aplicación está realizando consultas reales a la base de
datos. No sucede que haya tareas pendientes guardados, por lo que está en
blanco por ahora.
65
Agregar más características
Ahora que te has conectado a la base de datos usando Entity Framework
Core, estás listo para agregar más características a la aplicación. Primero,
harás posible agregar nuevas tareas usando un formulario.
66
Agregar nuevas tareas
El usuario agregará nuevas tareas con un simple formulario abajo de la lista
Agregar un formulario
La vista Views/Todo/Index.cshtml tiene un lugar asignado para el formulario
para agregar elementos:
67
Views/Todo/AddItemPartial.cshtml
@model TodoItem
action es remplazado con una ruta real hacia la acción AddItem que
crearas:
Views/Todo/Index.cshtml
68
Cuando un usuario presiona el botón Add sobre el formulario que creaste, su
navegador construirá un solicitud POST hacia /Todo/AddItem en tu
aplicación. Que por el momento no funcionara, porque no hay una acción
que pueda manejar la ruta /Todo/AddItem . Si intentas ahora, ASP.NET Core
regresara un error 404 Not Found
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddItem(TodoItem newItem)
{
if (!ModelState.IsValid)
{
return RedirectToAction("Index");
}
return RedirectToAction("Index");
}
69
El atributo [ValidateAntiForgeryToken] antes de la acción le dice a
ASP.NET Core que est debe buscar (y verificar) el código oculto de
verificación qu fue agregado al formulario por el tag helper asp-action .
Este es una importante medida de seguridad para prevenir falsificación de
petición en sitios cruzados. donde tus usuario no puedes ser engañados
enviando los datos del formulario a sitios maliciosos. El código de
verificación se asegura que la aplicación es actualmente la única que
muestra el formulario y recibe los datos del formulario.
70
para guardar una tarea en la base de datos desde el modelo que es
usado para enlazar la solicitud de entrada. Es a veces llamado un
binding model or a data transfer object
if (!ModelState.IsValid)
{
return RedirectToAction("Index");
}
71
El método AddItemAsync regresa true or false dependiendo el clic, a es
item fue agregado satisfactoriamente a la base. Si este falla por alguna
razón, la acción regresará un error HTTP 400 Bad Request junto con el
objeto que contiene un mensaje de error.
_context.Items.Add(newItem);
72
}
Pruebaló
Ejecuta la aplicación y agrega algunas tareas a tu lista mediante el
formulario. Debido a que las tareas son almacenadas en la base de datos,
estas estarán ahí incluso después de detener e iniciar la aplicación otra vez.
73
Completa los elementos con una casilla
de verificación
Agregar tareas a tú lista de tareas es genial, pero eventualmente necesitaras
también completar las cosas. En la vista Views/Todo/Index.cshtml , una
casilla de verificación es, mostrada para cada tarea:
Views/Todo/Index.cshtml
<td>
<form asp-action="MarkDone" method="POST">
<input type="checkbox" class="done-checkbox">
<input type="hidden" name="id" value="@item.Id">
</form>
</td>
Cuando el bucle foreach se ejecuta en la vista e imprime una fila para cada
tarea pendiente, existirá una copia de este formulario en cada fila. La entrada
oculta que contiene el ID de la tarea a realizar permite que el código de su
74
controlador indique qué casilla se marcó. (Sin él, podría indicar que se
marcó alguna casilla, pero no cuál.)
wwwroot/js/site.js
$(document).ready(function() {
function markCompleted(checkbox) {
checkbox.disabled = true;
75
Este código primero usa jQuery (a una librería de apoyo en JavaScript) para
adjuntar algo de código al evento click de todos las casillas de
verificación sobre la página con la clase CSS done-checkbox . Cuando una
casilla de verificación es presionada, la función markCompleted() es
ejecutada.
Enviar el formulario
Esto toma responsabilidad del la vista y el código del lado del cliente. Ahora
es tiempo de agregar una nueva acción
[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkDone(Guid id)
{
if (id == Guid.Empty)
{
return RedirectToAction("Index");
}
76
return RedirectToAction("Index");
}
77
Con la vista y el controlador actualizados, todo lo que queda es agregar el
método de servicio faltante.
Services/ITodoItemService.cs
Services/TodoItemService.cs
item.IsDone = true;
Este método usa Entity Framework Core y Where() para encontrar una
tarea por ID en la base de datos. El método SingleOrDefaultAsync()
Una vez que estas seguro que el item no es nulo, es una simple cuestión de
configurar la propiedad IsDone :
78
item.IsDone = true;
Probando
Ejecuta la aplicación y checa algunas tareas de la lista. Refrescar la página y
ellas desaparecerán completamente, porque el filtro Where() aplicado en el
método GetIncompleteItemsAsync() . Ahora mismo, la aplicación contiene
una sola, lista de tareas compartida. Seria mucho más util si mantuviera
registros de una lista de tareas individual para cada usuario. En el siguiente
capitulo, agregarás inicio de sesión y características de seguridad al
proyecto.
79
Seguridad e identidad
La seguridad es una de las principales preocupaciones de cualquier
aplicación web moderna o API. Es importante mantener seguros los datos de
sus usuarios o usuarios y fuera del alcance de los atacantes. Este es un tema
muy amplio, que involucra cosas como:
ASP.NET Core puede ayudar a que todo esto sea más fácil de implementar.
Los dos primeros (protección contra inyección de SQL y ataques de
falsificación de petición en sitios cruzados) ya están incorporados, y puede
agregar algunas líneas de código para habilitar el soporte de HTTPS. Este
capítulo se centrará principalmente en los aspectos de identidad** de
seguridad: manejo de cuentas de usuario, autenticación (inicio de sesión) de
sus usuarios de forma segura y toma de decisiones de autorización una vez
que se autentiquen.
80
la autenticación como una pregunta: "¿Sé quién? este usuario es? "
Mientras que la autorización pregunta: "¿Tiene este usuario permiso
para hacer X?"
81
82
Requerir autenticación
A menudo, deseará que el usuario inicie sesión antes de poder acceder a
ciertas partes de su aplicación. Por ejemplo, tiene sentido mostrar la página
de inicio a todos (ya sea que haya iniciado sesión o no), pero solo mostrar su
lista de tareas después de haber iniciado sesión.
Controllers/TodoController.cs
[Authorize]
public class TodoController : Controller
{
// ...
}
using Microsoft.AspNetCore.Authorization;
83
84
Usando la identidad en la aplicación
Los elementos de la lista de tareas pendientes todavía se comparten entre
todos los usuarios, porque las entidades de tareas pendientes almacenadas no
están vinculadas a un usuario en particular. Ahora que el atributo
[Authorize] asegura que debe iniciar sesión para ver la vista de tareas,
puede filtrar la consulta de la base de datos según quién haya iniciado
sesión.
Controllers/TodoController.cs
[Authorize]
public class TodoController : Controller
{
private readonly ITodoItemService _todoItemService;
private readonly UserManager<ApplicationUser> _userManager;
// ...
}
using Microsoft.AspNetCore.Identity;
85
La clase UserManager es parte de ASP.NET Core Identity. Puedes usarlo
para obtener al usuario actual en la acción Index :
return View(model);
}
disponible en la acción:
86
if (currentUser == null) return Challenge();
Services/ITodoItemService.cs
// ...
}
Services/TodoItemService
Models/TodoItem.cs
87
public string UserId { get; set; }
Esto crea una nueva migración llamada AddItemUserId que agregará una
nueva columna a la tabla Items , reflejando el cambio realizado en el
modelo TodoItem .
Services/TodoItemService.cs
88
Si ejecuta la aplicación y se registra o inicia sesión, verá una lista de tareas
vacía una vez más. Desafortunadamente, cualquier tarea que intentes agregar
desaparece en el éter, porque aún no has actualizado la acción AddItem para
que el usuario la tenga en cuenta.
Controllers/TodoController.cs
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddItem(TodoItem newItem)
{
if (!ModelState.IsValid)
{
return RedirectToAction("Index");
}
if (!successful)
{
return BadRequest("Could not add item.");
}
return RedirectToAction("Index");
}
[ValidateAntiForgeryToken]
public async Task<IActionResult> MarkDone(Guid id)
{
89
if (id == Guid.Empty)
{
return RedirectToAction("Index");
}
if (!successful)
{
return BadRequest("Could not mark item as done.");
}
return RedirectToAction("Index");
}
90
// ...
}
// ...
}
¡Todo listo! Intenta usar la aplicación con dos cuentas de usuario diferentes.
Las tareas pendientes se mantienen privadas para cada cuenta.
91
Autorización con roles
Los roles son un enfoque común para el manejo de permisos y
autorizaciones en una aplicación web. Por ejemplo, es común crear una rol
de administrador que otorgue a los usuarios administradores más permisos o
poder que los usuarios normales.
En este proyecto, agregará una página para Administrar usuarios que solo
los administradores pueden ver. Si los usuarios normales intentan acceder a
él, verán un error.
Controllers/ManageUsersController.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using AspNetCoreTodo.Models;
using Microsoft.EntityFrameworkCore;
namespace AspNetCoreTodo.Controllers
{
[Authorize(Roles = "Administrator")]
public class ManageUsersController : Controller
{
private readonly UserManager<ApplicationUser>
_userManager;
public ManageUsersController(
UserManager<ApplicationUser> userManager)
92
{
_userManager = userManager;
}
return View(model);
}
}
}
Models/ManageUsersViewModel.cs
using System.Collections.Generic;
namespace AspNetCoreTodo.Models
{
public class ManageUsersViewModel
{
public ApplicationUser[] Administrators { get; set; }
93
public ApplicationUser[] Everyone { get; set;}
}
}
Views/ManageUsers/Index.cshtml
@model ManageUsersViewModel
@{
ViewData["Title"] = "Manage users";
}
<h2>@ViewData["Title"]</h2>
<h3>Administrators</h3>
<table class="table">
<thead>
<tr>
<td>Id</td>
<td>Email</td>
</tr>
</thead>
<h3>Everyone</h3>
<table class="table">
<thead>
<tr>
94
<td>Id</td>
<td>Email</td>
</tr>
</thead>
95
Por razones obvias de seguridad, no es posible que nadie registre una nueva
cuenta de administrador. De hecho, el rol de administrador ni siquiera existe
en la base de datos todavía.
SeedData.cs
using System;
using System.Linq;
using System.Threading.Tasks;
using AspNetCoreTodo.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace AspNetCoreTodo
{
public static class SeedData
{
public static async Task InitializeAsync(
IServiceProvider services)
{
var roleManager = services
.GetRequiredService<RoleManager<IdentityRole>>();
await EnsureRolesAsync(roleManager);
96
El método InitializeAsync() utiliza un IServiceProvider (la colección de
servicios que se configura en el método Startup.ConfigureServices() ) para
obtener el RoleManager y el UserManager de ASP.NET Core Identity.
if (alreadyExists) return;
await roleManager.CreateAsync(
new IdentityRole(Constants.AdministratorRole));
}
Constants.cs
namespace AspNetCoreTodo
{
public static class Constants
{
public const string AdministratorRole = "Administrator";
}
}
97
A continuación, escriba el método EnsureTestAdminAsync() :
SeedData.cs
Program.cs
98
InitializeDatabase(host);
host.Run();
}
try
{
SeedData.InitializeAsync(services).Wait();
}
catch (Exception ex)
{
var logger = services
.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Error occurred seeding the DB.");
}
}
}
using Microsoft.Extensions.DependencyInjection;
99
por razones técnicas no puedes usar "esperar" en la clase "Programa".
Esta es una rara excepción. ¡Debes usar await en cualquier otro
lugar!
Views/Shared/_AdminActionsPartial.cshtml
@using Microsoft.AspNetCore.Identity
@using AspNetCoreTodo.Models
100
@if (signInManager.IsSignedIn(User))
{
var currentUser = await userManager.GetUserAsync(User);
if (isAdmin)
{
<ul class="nav navbar-nav navbar-right">
<li>
<a asp-controller="ManageUsers"
asp-action="Index">
Manage Users
</a>
</li>
</ul>
}
}
Views/Shared/_Layout.cshtml
101
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<!-- existing code here -->
</ul>
@await Html.PartialAsync("_LoginPartial")
@await Html.PartialAsync("_AdminActionsPartial")
</div>
Cuando inicie sesión con una cuenta de administrador, ahora verá un nuevo
elemento en la parte superior derecha:
102
Más recursos
ASP.NET Core Identity le ayuda a agregar las características de seguridad e
identidad como inicio de sesión y registro a su aplicación. Las plantillas
dotnet new le brindan vistas y controladores predefinidos que manejan
estos escenarios comunes para que pueda comenzar a trabajar rápidamente.
Hay mucho más que puede hacer ASP.NET Core Identity, como restablecer
la contraseña y el inicio de sesión social. La documentación disponible en
http://docs.asp.net es un recurso fantástico para aprender a agregar estas
funciones.
103
Además, los datos confidenciales del usuario no se almacenan en su
propia base de datos.
Para este proyecto, ASP.NET Core Identity es una excelente opción. Para
proyectos más complejos, recomiendo investigar un poco y experimentar
con ambas opciones para comprender cuál es la mejor para su caso de uso.
104
Pruebas automatizadas
Escribir pruebas es una parte importante del desarrollo de cualquier
aplicación. Probar su código lo ayuda a encontrar y evitar errores, y
posteriormente facilita la refactorización de su código sin descomponer la
funcionalidad o introducir nuevos problemas.
105
Pruebas unitarias
Las pruebas unitarias son pruebas pequeñas y cortas que verifican el
comportamiento de un solo método o clase. Cuando el código que está
probando se basa en otros métodos o clases, las pruebas unitarias se basan
en simulardores de esas otras clases para que la prueba solo se enfoque en
una cosa a la vez.
Cuando escribe una prueba de unitaria, por otro lado, tiene que manejar
usted mismo el gráfico de dependencia. Es típico proporcionar versiones
solo de prueba o "simuladas" de esas dependencias. Esto significa que puede
aislar solo la lógica en la clase o el método que está probando. (¡Esto es
importante! Si está probando un servicio, no quiere también estar
escribiendo accidentalmente en su base de datos).
106
de su proyecto principal.
AspNetCoreTodo/
AspNetCoreTodo/
AspNetCoreTodo.csproj
Controllers/
(etc...)
AspNetCoreTodo.UnitTests/
AspNetCoreTodo.UnitTests.csproj
107
Si está utilizando Visual Studio Code, es posible que deba cerrar y
volver a abrir la ventana de Visual Studio Code para la compleción
del código funcione en el nuevo proyecto.
_context.Items.Add(newItem);
false )
El título del nuevo elemento debe copiarse de newItem.Title
108
Imagínese si usted o alguien más reformuló el método AddItemAsync() y se
olvidó de parte de esta lógica de negocios. ¡El comportamiento de su
aplicación podría cambiar sin que usted se dé cuenta! Puede evitar esto
escribiendo una prueba que verifique que esta lógica de negocios no haya
cambiado (incluso si la implementación interna del método cambia).
AspNetCoreTodo.UnitTests/TodoItemServiceShould.cs
using System;
using System.Threading.Tasks;
using AspNetCoreTodo.Data;
using AspNetCoreTodo.Models;
using AspNetCoreTodo.Services;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace AspNetCoreTodo.UnitTests
{
public class TodoItemServiceShould
{
[Fact]
public async Task AddNewItemAsIncompleteWithDueDate()
{
// ...
}
}
}
109
Hay muchas maneras diferentes de nombrar y organizar pruebas,
todas con diferentes pros y contras. Me gusta después de fijar mis
clases de prueba con Debería para crear una oración legible con el
nombre del método de prueba, ¡pero puede usar su propio estilo!
110
La última línea crea un tarea llamado "¿Pruebas?", Y le dice al servicio que
lo guarde en la base de datos (en memoria).
111
Confirmar un valor de fecha y hora es un poco complicado, ya que la
comparación de dos fechas para la igualdad fallará incluso si los
componentes de milisegundos son diferentes. En su lugar, la prueba verifica
que el valor DueAt esté a menos de un segundo del valor esperado.
Ejecutar la prueba
En la terminal, ejecute este comando (asegúrese de que todavía esté en el
directorio AspNetCoreTodo.UnitTests ):
dotnet test
112
El método GetIncompleteItemsAsync() devuelve solo las tareas que
pertenecen a un usuario en particular
113
Pruebas de integración
En comparación con las pruebas unitarias, las pruebas de integración tienen
un alcance mucho mayor. Prueba toda la pila de aplicaciones. En lugar de
aislar una clase o método, las pruebas de integración aseguran que todos los
componentes de su aplicación estén funcionando juntos correctamente:
enrutamiento, controladores, servicios, código de base de datos, etc.
Las pruebas de integración son más lentas y más complejas que las pruebas
de unitarias, por lo que es común que un proyecto tenga muchas pruebas de
unitarias pequeñas pero solo un puñado de pruebas de integración.
114
AspNetCoreTodo/
AspNetCoreTodo/
AspNetCoreTodo.csproj
Controllers/
(etc...)
AspNetCoreTodo.UnitTests/
AspNetCoreTodo.UnitTests.csproj
AspNetCoreTodo.IntegrationTests/
AspNetCoreTodo.IntegrationTests.csproj
Elimine el archivo UnitTest1.cs creado por dotnet new . Estás listo para
escribir una prueba de integración.
115
AspNetCoreTodo.IntegrationTests/TestFixture.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
namespace AspNetCoreTodo.IntegrationTests
{
public class TestFixture : IDisposable
{
private readonly TestServer _server;
public TestFixture()
{
var builder = new WebHostBuilder()
.UseStartup<AspNetCoreTodo.Startup>()
.ConfigureAppConfiguration((context, config) =>
{
config.SetBasePath(Path.Combine(
Directory.GetCurrentDirectory(),
"..\\..\\..\\..\\AspNetCoreTodo"));
config.AddJsonFile("appsettings.json");
});
Client = _server.CreateClient();
Client.BaseAddress = new Uri("http://localhost:8888");
}
116
}
}
Ahora estás (realmente) listo para escribir una prueba de integración. Crea
una nueva clase llamada TodoRouteShould :
AspNetCoreTodo.IntegrationTests/TodoRouteShould.cs
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace AspNetCoreTodo.IntegrationTests
{
public class TodoRouteShould : IClassFixture<TestFixture>
{
private readonly HttpClient _client;
[Fact]
public async Task ChallengeAnonymousUser()
{
// Arrange
var request = new HttpRequestMessage(
HttpMethod.Get, "/todo");
117
response.StatusCode);
Assert.Equal(
"http://localhost:8888/Account" +
"/Login?ReturnUrl=%2Ftodo",
response.Headers.Location.ToString());
}
}
}
Esta prueba realiza una solicitud anónima (sin iniciar sesión) a la ruta
/todo y verifica que el navegador se redirige a la página de inicio de
sesión.
Ejecutar la prueba
Ejecute la prueba en el terminal con dotnet test . Si todo funciona bien,
verás un mensaje de éxito:
118
Resumiendo
Las pruebas son un tema amplio y hay mucho más que aprender. Este
capítulo no toca el código de prueba de interfaz de usuario (UI) ni el código
de prueba (JavaScript), que probablemente merecen libros completos por su
cuenta. Sin embargo, debe tener las habilidades y el conocimiento básico
que necesita para aprender más sobre las pruebas y practicar la escritura de
pruebas para sus propias aplicaciones.
119
Desplegar la aplicación
Has recorrido un largo camino, pero aún no has terminado. Una vez que has
creado una gran aplicación, ¡debes compartirla con el mundo!
Opciones de implementación
Las aplicaciones de ASP.NET Core se implementan normalmente en uno de
estos entornos:
120
Windows. Puede usar el servidor web IIS en Windows para alojar
aplicaciones ASP.NET Core. Por lo general, es más fácil (y más barato)
implementarlo en Azure, pero si prefiere administrar los servidores de
Windows usted mismo, funcionará bien.
121
Desplegar en Azure
La implementación de la aplicación de ASP.NET Core en Azure solo lleva
unos pocos pasos. Puede hacerlo a través del portal web de Azure o en la
línea de comandos utilizando la CLI de Azure. Voy a cubrir este último.
Lo que necesitarás
Git (usa git --version para asegurarte de que esté instalado)
El CLI de Azure (siga las instrucciones de instalación en
https://github.com/Azure/azure-cli)
Una suscripción de Azure (la suscripción gratuita está bien)
Un archivo de configuración de implementación en la raíz de su
proyecto.
.deployment
[config]
project = AspNetCoreTodo/AspNetCoreTodo.csproj
122
Asegúrese de guardar el archivo como .deployment sin otras partes en el
nombre. (En Windows, puede que tenga que poner comillas alrededor del
nombre del archivo, como ".deployment" , para evitar que se agregue una
extensión .txt .)
.deployment
AspNetCoreTodo
AspNetCoreTodo.IntegrationTests
AspNetCoreTodo.UnitTests
az login
para obtener una lista de ubicaciones y encontrar una más cercana a usted.
123
F1 es el plan de aplicación gratuita. Si desea usar un nombre de
dominio personalizado con su aplicación, use el plan D1 ($ 10 / mes)
o superior.
git init
git add .
git commit -m "First commit!"
Siga las instrucciones para crear una contraseña. Luego usa config-local-
124
https://nate@mytodoapp.scm.azurewebsites.net/MyTodoApp.git
Solo necesitas hacer estos pasos una vez. Ahora, cuando quiera enviar sus
archivos de aplicaciones a Azure, verifíquelos con Git y ejecute
125
Desplegando con Docker
Si no está utilizando una plataforma como Azure, las tecnologías de
contenedorización como Docker pueden facilitar la implementación de
aplicaciones web en sus propios servidores. En lugar de dedicar tiempo a
configurar un servidor con las dependencias que necesita para ejecutar su
aplicación, copiar archivos y reiniciar procesos, simplemente puede crear
una imagen de Docker que describa todo lo que su aplicación necesita para
ejecutar y girarla como un contenedor en cualquier Docker. anfitrión.
docker version
126
Esto le dice a Docker que use la imagen microsoft/dotnet:2.0-sdk como
punto de partida. Esta imagen es publicada por Microsoft y contiene las
herramientas y dependencias que necesita para ejecutar dotnet build y
compilar su aplicación. Al utilizar esta imagen preconstruida como punto de
partida, Docker puede optimizar la imagen producida para su aplicación y
mantenerla pequeña.
WORKDIR /app/AspNetCoreTodo
RUN dotnet restore
127
RUN dotnet publish -o out /p:PublishWithAspNetCoreTargetManifest="fal
se"
Dockerfile
128
RUN dotnet restore
COPY AspNetCoreTodo/. ./
RUN dotnet publish -o out /p:PublishWithAspNetCoreTargetManifest="fal
se"
Una vez creada la imagen, puede ejecutar docker images para listar todas
las imágenes disponibles en su máquina local. Para probarlo en un
contenedor, ejecute
129
¿Recuerda la variable ASPNETCORE_URLS que le dijo a ASP.NET Core que
escuche en el puerto 80? La opción -p 8080: 80 le dice a Docker que
asigne el puerto 8080 en su máquina al puerto del contenedor 80. Abra su
navegador y navegue a http://localhost:8080 para ver la aplicación que se
ejecuta en el contenedor !
Configurar Nginx
Al principio de este capítulo, mencioné que debería usar un proxy inverso
como Nginx para enviar las solicitudes a Kestrel. También puedes usar
Docker para esto.
mkdir nginx
nginx/Dockerfile
FROM nginx
COPY nginx.conf /etc/nginx/nginx.conf
nginx/nginx.conf
130
http {
server {
listen 80;
location / {
proxy_pass http://kestrel:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'keep-alive';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
}
https://github.com/aspnet/Announcements/issues/295
compose.yml :
docker-compose.yml
nginx:
build: ./nginx
links:
- kestrel:kestrel
ports:
- "80:80"
131
kestrel:
build: .
ports:
- "80"
docker-compose up
Me gusta usar DigitalOcean porque han hecho que sea muy fácil comenzar.
DigitalOcean tiene una máquina virtual Docker pre-construida y tutoriales
detallados para que Docker esté en funcionamiento (busque "docker
digitalocean").
132
133
Conclusión
Gracias por llegar hasta el final de El pequeño libro de ASP.NET Core!. Si
este fue útil (o no) me encantaría escuchar tus comentarios. Enviame tus
comentarios via Twitter:https://twitter.com/nbarbettini
134
disponible y necesitas un cupón, enviame un correo electrónico a
nate@barbettini.com.
Happy coding!
Agradecimiento especiales
Para Jennifer, quien siempre apoya mis locas ideas.
0xNF
Matt Welke [welkie]
Raman Zhylich [zhilich]
sahinyanlik (Turco)
135
windsing, yuyi (Chino simplificado)
Registro de cambios
El registro completo y detallado esta siempre disponible aquí:
https://github.com/nbarbettini/little-aspnetcore-book/releases
136
137